diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 2ff173cc8..b47a908e8 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -8,6 +8,7 @@ on: push: branches: - main + pull_request: release: types: [published] @@ -22,7 +23,7 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 + - os: ubuntu-22.04 # target: x86_64-unknown-linux-gnu archive: tar.gz binary_name: opsml-server @@ -34,12 +35,6 @@ jobs: binary_name: opsml-server archive_name: opsml-server-aarch64-linux-gnu - - os: macos-latest - target: aarch64-apple-darwin - archive: zip - binary_name: opsml-server - archive_name: opsml-server-aarch64-darwin - - os: macos-13 target: x86_64-apple-darwin archive: zip @@ -78,19 +73,23 @@ jobs: with: python-version: ${{ env.INTERPRETER }} - - name: Install UI dependencies - run: | - make install.ui.deps - make build.ui - - name: Update apt repositories (Linux) if: contains(matrix.os, 'ubuntu') run: | sudo apt-get update -y sudo apt-get install -y build-essential + - name: Install UI dependencies + run: | + # install all deps and build UI + make build.ui + + # remove node_modules and rebuild prod version to reduce size + make install.ui.deps.prod + - name: Build Binaries - run: cargo build -p opsml-server --release --target ${{ matrix.target }} + run: | + cargo build -p opsml-server --release --target ${{ matrix.target }} - name: Prepare binary directory shell: bash @@ -102,6 +101,12 @@ jobs: cp target/${{ matrix.target }}/release/${{ matrix.binary_name }} release-bin/ chmod +x release-bin/${{ matrix.binary_name }} fi + # Copy UI build for Docker builds + if [ "${{ matrix.target }}" == "x86_64-unknown-linux-gnu" ] || [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then + mkdir -p release-bin/ui + cp -r crates/opsml_server/opsml_ui/build release-bin/ui/build || true + cp -r crates/opsml_server/opsml_ui/node_modules release-bin/ui/node_modules || true + fi - name: Create zip archive (Windows/macOS) if: contains(matrix.archive, 'zip') @@ -128,6 +133,28 @@ jobs: path: ${{ matrix.archive_name }}.${{ matrix.archive }} retention-days: 1 + - name: Package UI for Node.js distribution + if: matrix.target == 'x86_64-unknown-linux-gnu' + shell: bash + run: | + mkdir -p packaged-ui + cp -r crates/opsml_server/opsml_ui/build packaged-ui/build + cp -r crates/opsml_server/opsml_ui/node_modules packaged-ui/node_modules + cp crates/opsml_server/opsml_ui/package.json packaged-ui/ + cp crates/opsml_server/opsml_ui/pnpm-lock.yaml packaged-ui/ || true + cd packaged-ui + zip -r ../opsml-ui-node.zip ./* + + # only want to run this once, so only on the first matrix item + # The artifact will be the same on all builds regardless of target + - name: Upload UI Node.js package artifact + if: matrix.target == 'x86_64-unknown-linux-gnu' + uses: actions/upload-artifact@v4 + with: + name: opsml-ui-node + path: opsml-ui-node.zip + retention-days: 1 + publish-docker-images: if: github.event_name == 'release' needs: build @@ -140,14 +167,13 @@ jobs: include: - image: "ubuntu" tag_suffix: "ubuntu" - - image: "alpine" - tag_suffix: "alpine" - - image: "scratch" - tag_suffix: "scratch" + artifact: "opsml-server-x86_64-linux-gnu" - image: "debian" tag_suffix: "debian" - - image: "distroless" - tag_suffix: "distroless" + artifact: "opsml-server-x86_64-linux-gnu" + - image: "rocky" + tag_suffix: "rocky-minimal" + artifact: "opsml-server-x86_64-linux-gnu" steps: - name: Checkout Code @@ -164,12 +190,15 @@ jobs: - name: Extract binary run: | - mkdir -p binary - tar -xzf ./artifacts/opsml-server-x86_64-linux-gnu.tar.gz -C ./binary - - - name: Set up binary permissions - run: | - chmod +x ./binary/opsml-server + archive="./artifacts/${{ matrix.artifact }}.tar.gz" + if [ -f "$archive" ]; then + mkdir -p binary + tar -xzf "$archive" -C ./binary + chmod +x ./binary/opsml-server + else + echo "x86_64 binary $archive not found, skipping this build" + exit 1 + fi - name: Set version tag id: set-version @@ -193,7 +222,7 @@ jobs: file: docker/official/${{ matrix.image }}/Dockerfile push: true build-args: | - OPSML_SERVER_BINARY=./binary/opsml-server + OPSML_SERVER_BINARY=./binary tags: | demml/opsml:${{ matrix.tag_suffix }}-amd64-${{ steps.set-version.outputs.VERSION }} ${{ github.event_name == 'release' && format('demml/opsml:{0}-amd64-latest', matrix.tag_suffix) || '' }} @@ -212,14 +241,13 @@ jobs: include: - image: "ubuntu" tag_suffix: "ubuntu" - - image: "alpine" - tag_suffix: "alpine" - - image: "scratch" - tag_suffix: "scratch" + artifact: "opsml-server-aarch64-linux-gnu" - image: "debian" tag_suffix: "debian" - - image: "distroless" - tag_suffix: "distroless" + artifact: "opsml-server-aarch64-linux-gnu" + - image: "rocky" + tag_suffix: "rocky-minimal" + artifact: "opsml-server-aarch64-linux-gnu" steps: - name: Checkout Code @@ -239,15 +267,15 @@ jobs: - name: Extract binary run: | - if [ -f "./artifacts/opsml-server-aarch64-linux-gnu.tar.gz" ]; then + archive="./artifacts/${{ matrix.artifact }}.tar.gz" + if [ -f "$archive" ]; then mkdir -p binary - tar -xzf ./artifacts/opsml-server-aarch64-linux-gnu.tar.gz -C ./binary + tar -xzf "$archive" -C ./binary chmod +x ./binary/opsml-server else - echo "ARM64 binary not found, skipping this build" + echo "ARM64 binary $archive not found, skipping this build" exit 1 fi - continue-on-error: true - name: Set version tag id: set-version @@ -272,7 +300,7 @@ jobs: push: true platforms: linux/arm64 build-args: | - OPSML_SERVER_BINARY=./binary/opsml-server + OPSML_SERVER_BINARY=./binary tags: | demml/opsml:${{ matrix.tag_suffix }}-arm64-${{ steps.set-version.outputs.VERSION }} ${{ github.event_name == 'release' && format('demml/opsml:{0}-arm64-latest', matrix.tag_suffix) || '' }} diff --git a/Cargo.lock b/Cargo.lock index 3cf7d542a..f4ee9aee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4412,7 +4412,6 @@ dependencies = [ "password-auth", "rand 0.9.2", "reqwest", - "rust-embed", "rusty-logging", "scouter-client", "semver", @@ -5847,40 +5846,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rust-embed" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.106", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" -dependencies = [ - "sha2", - "walkdir", -] - [[package]] name = "rustc-demangle" version = "0.1.26" diff --git a/Cargo.toml b/Cargo.toml index 38a0294f9..090ea96a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,6 @@ rand = { version = "0.9.1"} rayon = "1.*" regex = "1.*" reqwest = { version = "0.12.*", features = ["json", "stream", "multipart", "rustls-tls", "rustls-tls-native-roots", "blocking" ], default-features = false } -rust-embed = "8.*" reqwest-middleware = "0.*" rusty-logging = "0.*" # we cant use crates.io because scouter relies on a fork of linfa at the moment diff --git a/Makefile b/Makefile index b3c75cdd3..19088cde5 100644 --- a/Makefile +++ b/Makefile @@ -121,11 +121,17 @@ ui.update.deps: install.ui.deps: cd $(UI_DIR) && pnpm install -.PHONY: ui.build +.PHONY: ui.install.deps.prod +install.ui.deps.prod: + # remove existing node_modules + rm -rf $(UI_DIR)/node_modules + # install only production dependencies + cd $(UI_DIR) && pnpm install --prod + +.PHONY: build.ui build.ui: cd $(UI_DIR) && pnpm install cd $(UI_DIR) && pnpm build - touch $(UI_DIR)/site/.gitkeep # to make sure the site folder is not ignored by git ui.dev: cd $(UI_DIR) && pnpm run dev @@ -139,3 +145,46 @@ prepend.changelog: # get version from Cargo.toml @VERSION=$(shell grep '^version =' Cargo.toml | cut -d '"' -f 2) && \ git cliff --unreleased --tag $$VERSION --prepend CHANGELOG.md + + +###### Development & Production - Separate Servers ###### + +.PHONY: dev.backend +dev.backend: + cargo build -p opsml-server + OPSML_SERVER_PORT=8080 ./target/debug/opsml-server + +.PHONY: dev.frontend +dev.frontend: + cd $(UI_DIR) && pnpm run dev + +.PHONY: build.backend +build.backend: + cargo build -p opsml-server --target + +.PHONY: start.backend +start.backend: build.backend + OPSML_SERVER_PORT=8080 ./target/release/opsml-server + +.PHONY: start.frontend +start.frontend: build.ui + cd $(UI_DIR) && node build/index.js + +.PHONY: dev.both +dev.both: + @echo "Starting both servers in development mode..." + @echo "Backend API: http://localhost:8080" + @echo "Frontend SSR: http://localhost:3000" + @make -j2 dev.backend dev.frontend + +.PHONY: start.both +start.both: + @echo "Starting both servers in production mode..." + @echo "Backend API: http://localhost:8080" + @echo "Frontend SSR: http://localhost:3000" + @make -j2 dev.backend start.frontend + +.PHONY: stop.both +stop.both: + -lsof -ti:3000 | xargs kill -9 2>/dev/null || true + -lsof -ti:8080 | xargs kill -9 2>/dev/null || true diff --git a/crates/opsml_cli/Cargo.toml b/crates/opsml_cli/Cargo.toml index 9cbe4bee1..7950901d8 100644 --- a/crates/opsml_cli/Cargo.toml +++ b/crates/opsml_cli/Cargo.toml @@ -40,6 +40,8 @@ flate2 = "1.*" tar = "*" + + [dev-dependencies] mockall = { workspace = true } mockito = { workspace = true } diff --git a/crates/opsml_cli/src/actions/ui.rs b/crates/opsml_cli/src/actions/ui.rs index 08ba309b8..1fed84a14 100644 --- a/crates/opsml_cli/src/actions/ui.rs +++ b/crates/opsml_cli/src/actions/ui.rs @@ -10,16 +10,27 @@ use std::{ use sysinfo::{Pid, System}; const GITHUB_REPO: &str = "demml/opsml"; + +// Cache directory const CACHE_DIR: &str = ".opsml-cache"; const PID_FILE: &str = "opsml-server.pid"; -pub fn save_process_id(process: &Child) -> Result<(), UiError> { - let pid_path = get_pid_file_path()?; +// UI archive +const UI_ARCHIVE_NAME: &str = "opsml-ui-node.zip"; +const UI_PID_FILE: &str = "opsml-ui.pid"; + +pub fn save_process_id(process: &Child, is_ui: bool) -> Result<(), UiError> { + let pid_path = get_pid_file_path(is_ui)?; fs::write(&pid_path, process.id().to_string()).map_err(UiError::ProcessIdWriteError) } -fn get_pid_file_path() -> Result { +fn get_pid_file_path(is_ui: bool) -> Result { let cache_dir = get_cache_dir()?; + + if is_ui { + return Ok(cache_dir.join(UI_PID_FILE)); + } + Ok(cache_dir.join(PID_FILE)) } @@ -40,40 +51,101 @@ pub enum Platform { /// /// # Arguments /// * `version` - The version of the OpsML UI to start. If not provided, the latest version will be used. -pub fn start_ui(version: &str, artifact_url: Option) -> Result<(), UiError> { +pub fn start_ui( + version: &str, + server_artifact_url: &Option, + ui_artifact_url: &Option, + dev_mode: &bool, +) -> Result<(), UiError> { + // if dev mode, just start the UI from the local directory + // this assumes the user is running the command from opsml/py-opsml directory + if *dev_mode { + // start the backend server from the local opsml_server director + // always at ./target/debug/opsml-server of the current directory + + // navigate to parent directory (should be opsml root) + let parent_dir = env::current_dir() + .map_err(UiError::CurrentDirError)? + .parent() + .unwrap() + .to_path_buf(); + let server_path = parent_dir.join("target").join("debug").join("opsml-server"); + execute_binary(&server_path)?; + + // start the ui from the local opsml_ui directory + let ui_path = parent_dir + .join("crates") + .join("opsml_server") + .join("opsml_ui"); + execute_ui(&ui_path)?; + return Ok(()); + } + let platform = detect_platform()?; let cache_dir = get_cache_dir()?; let binary_path = cache_dir.join(format!("opsml-server-v{version}")); + let ui_path = cache_dir.join(format!("opsml-ui-v{version}")); if !binary_path.exists() { - download_binary(&platform, version, &cache_dir, artifact_url)?; + download_binary(&platform, version, &cache_dir, server_artifact_url)?; cleanup_old_binaries(&cache_dir, version, &platform)?; } + if !ui_path.exists() { + download_ui_package(version, &cache_dir, ui_artifact_url)?; + cleanup_old_ui_packages(&cache_dir, version)?; + } + + // this will start the binary in a new process execute_binary(&binary_path)?; + + // execute the UI package + execute_ui(&ui_path)?; + Ok(()) } +/// Stop both the backend server and UI pub fn stop_ui() -> Result<(), UiError> { - let pid_path = get_pid_file_path()?; + let cache_dir = get_cache_dir()?; - if !pid_path.exists() { - return Err(UiError::NoRunningServer); + // Stop backend server + let backend_pid_path = cache_dir.join(PID_FILE); + if backend_pid_path.exists() { + let pid_str = fs::read_to_string(&backend_pid_path).map_err(UiError::ProcessIdReadError)?; + let pid: usize = pid_str + .trim() + .parse() + .map_err(UiError::ProcessIdParseError)?; + + let s = System::new_all(); + if let Some(process) = s.process(Pid::from(pid)) { + println!("Stopping OpsML backend server (PID: {pid})"); + process + .kill_and_wait() + .map_err(|_| UiError::ProcessKillError(format!("{pid}")))?; + } + fs::remove_file(&backend_pid_path).ok(); // Clean up PID file } - let pid_str = fs::read_to_string(&pid_path).map_err(UiError::ProcessIdReadError)?; - - let pid: usize = pid_str - .trim() - .parse() - .map_err(UiError::ProcessIdParseError)?; + // Stop UI server - TODO: combine with above loop later + let ui_pid_path = cache_dir.join(UI_PID_FILE); + if ui_pid_path.exists() { + let pid_str = fs::read_to_string(&ui_pid_path).map_err(UiError::ProcessIdReadError)?; + let pid: usize = pid_str + .trim() + .parse() + .map_err(UiError::ProcessIdParseError)?; + + let s = System::new_all(); + if let Some(process) = s.process(Pid::from(pid)) { + println!("Stopping OpsML UI server (PID: {pid})"); + process + .kill_and_wait() + .map_err(|_| UiError::ProcessKillError(format!("{pid}")))?; + } - let s = System::new_all(); - if let Some(process) = s.process(Pid::from(pid)) { - println!("Stopping OpsML UI server (PID: {pid})"); - process - .kill_and_wait() - .map_err(|_| UiError::ProcessKillError(format!("{pid}")))?; + fs::remove_file(&ui_pid_path).ok(); // Clean up PID file } Ok(()) @@ -112,6 +184,7 @@ fn get_cache_dir() -> Result { /// * `platform` - The platform to download the binary for /// * `version` - The version of the binary to download /// * `cache_dir` - The directory to cache the downloaded binary +/// * `artifact_url` - Optional URL to download the binary from /// /// # Returns /// A Result indicating success or failure @@ -119,7 +192,7 @@ fn download_binary( platform: &Platform, version: &str, cache_dir: &Path, - artifact_url: Option, + artifact_url: &Option, ) -> Result<(), UiError> { // standardize naming let archive_name = match platform { @@ -128,9 +201,12 @@ fn download_binary( Platform::Linux(arch) => format!("opsml-server-{arch}-linux-gnu.tar.gz"), }; - let url = artifact_url.unwrap_or(format!( - "https://github.com/{GITHUB_REPO}/releases/download/v{version}/{archive_name}", - )); + let url = match artifact_url { + Some(url) => url.clone(), + None => { + format!("https://github.com/{GITHUB_REPO}/releases/download/v{version}/{archive_name}") + } + }; let response = reqwest::blocking::get(&url).map_err(UiError::DownloadBinaryError)?; let archive_path = cache_dir.join(&archive_name); @@ -192,6 +268,115 @@ fn download_binary( Ok(()) } +/// DOwnload and extract the OpsML UI package +/// +/// # Arguments +/// * `version` - The version of the UI package to download +/// * `cache_dir` - The cache directory to store the UI package +/// * `ui_artifact_url` - Optional URL to download the UI package from +/// # Returns +/// A Result indicating success or failure +fn download_ui_package( + version: &str, + cache_dir: &Path, + ui_artifact_url: &Option, +) -> Result<(), UiError> { + let url = match ui_artifact_url { + Some(url) => url.clone(), + None => format!( + "https://github.com/{GITHUB_REPO}/releases/download/v{version}/{UI_ARCHIVE_NAME}" + ), + }; + + println!("Downloading UI package for version {version}..."); + + let response = reqwest::blocking::get(&url).map_err(UiError::DownloadBinaryError)?; + let archive_path = cache_dir.join(UI_ARCHIVE_NAME); + + let bytes = response.bytes().map_err(UiError::DownloadBinaryError)?; + fs::write(&archive_path, bytes).map_err(UiError::WriteBinaryError)?; + + // Create versioned UI directory + let ui_dir = cache_dir.join(format!("opsml-ui-v{version}")); + fs::create_dir_all(&ui_dir).map_err(UiError::CreateCacheDirError)?; + + // Extract UI package (always zip format) + let reader = fs::File::open(&archive_path).map_err(UiError::ArchiveOpenError)?; + let mut archive = zip::ZipArchive::new(reader).map_err(UiError::ArchiveZipError)?; + + archive + .extract(&ui_dir) + .map_err(UiError::ZipArchiveExtractionError)?; + + // Clean up archive + fs::remove_file(archive_path).map_err(UiError::RemoveArchiveError)?; + + println!("UI package extracted to: {}", ui_dir.display()); + Ok(()) +} + +/// Execute the UI package with Node.js. Sveltekit SSR apps typically require Node.js +/// We also run the server on port 3000 +/// +/// # Arguments +/// * `ui_dir` - The directory containing the extracted UI package +fn execute_ui(ui_dir: &Path) -> Result<(), UiError> { + // Check if Node.js is available + let node_check = Command::new("node").arg("--version").output(); + + if node_check.is_err() { + return Err(UiError::NodeNotFound); + } + + // Look for package.json to determine how to start the UI + let package_json_path = ui_dir.join("package.json"); + if !package_json_path.exists() { + return Err(UiError::PackageJsonNotFound); + } + + println!("Starting UI server with Node.js on port 3000..."); + + // Start the UI server process + // Assuming the UI can be started with "node build/server.js" + let ui_process = Command::new("node") + .current_dir(ui_dir) + .arg("build/index.js") + .spawn() + .or_else(|_| { + // Fallback: try npm start + Command::new("npm").current_dir(ui_dir).arg("start").spawn() + }) + .map_err(UiError::UiSpawnError)?; + + // Save the UI process ID + save_process_id(&ui_process, true)?; + + let ui_id = ui_process.id(); + println!("Started OpsML UI server (PID: {ui_id}) on http://localhost:3000"); + + Ok(()) +} + +/// Cleans up old UI packages from the cache directory +fn cleanup_old_ui_packages(cache_dir: &Path, current_version: &str) -> Result<(), UiError> { + let prefix = "opsml-ui-v"; + let current_ui_dir = format!("{prefix}{current_version}"); + + for entry in fs::read_dir(cache_dir).map_err(UiError::ReadError)? { + let entry = entry.map_err(UiError::ReadError)?; + let path = entry.path(); + + if let Some(dir_name) = path.file_name().and_then(|f| f.to_str()) { + // Check if it's a UI directory and not the current version + if path.is_dir() && dir_name.starts_with(prefix) && dir_name != current_ui_dir { + fs::remove_dir_all(&path).map_err(UiError::RemoveFileError)?; + } + } + } + + Ok(()) +} + ///// Execute the OpsML UI binary /// /// # Arguments @@ -208,7 +393,7 @@ fn execute_binary(binary_path: &Path) -> Result<(), UiError> { .map_err(UiError::BinarySpawnError)?; // Save the process ID - save_process_id(&child_process)?; + save_process_id(&child_process, false)?; let id = child_process.id(); @@ -423,7 +608,7 @@ mod tests { ); // Call download_binary directly for more focused testing - download_binary(&platform, version, cache_path, Some(full_mock_url))?; + download_binary(&platform, version, cache_path, &Some(full_mock_url))?; // Assert that the binary was extracted and renamed correctly let expected_binary_path = cache_path.join(format!("opsml-server-v{version}")); @@ -457,7 +642,7 @@ mod tests { mock_server.url, version, archive_name ); - download_binary(&platform, version, cache_path, Some(full_mock_url))?; + download_binary(&platform, version, cache_path, &Some(full_mock_url))?; // Windows binaries have .exe extension let expected_binary_path = cache_path.join(format!("opsml-server-v{}.exe", version)); @@ -500,7 +685,7 @@ mod tests { ); // download_binary contains the cfg logic for extraction, so we call it directly - download_binary(&platform, version, cache_path, Some(full_mock_url))?; + download_binary(&platform, version, cache_path, &Some(full_mock_url))?; // Linux binaries have no extension in this setup let expected_binary_path = cache_path.join(format!("opsml-server-v{version}")); diff --git a/crates/opsml_cli/src/cli/arg.rs b/crates/opsml_cli/src/cli/arg.rs index 329c0feb8..26ee2c093 100644 --- a/crates/opsml_cli/src/cli/arg.rs +++ b/crates/opsml_cli/src/cli/arg.rs @@ -254,4 +254,29 @@ pub struct UiArgs { /// Version #[arg(long = "version")] pub version: Option, + + /// Server binary url + /// Optional URL to download the opsml-server binary from. This is typically used for testing purposes. + #[arg(long = "server-url")] + pub server_url: Option, + + /// UI binary url + /// Optional URL to download the opsml-ui binary from. This is typically used for testing purposes. + #[arg(long = "ui-url")] + pub ui_url: Option, + + /// Development mode + /// If set, the UI will execute the local debug server build and the ui build located in crates/opsml_server/opsml_ui + /// This is only intended for development purposes. + #[arg(long = "dev-mode", default_value = "false")] + pub dev_mode: bool, +} + +#[derive(Args, Clone)] +pub struct StopUiArgs { + /// Development mode + /// If set, the UI will execute the local debug server build and the ui build located in crates/opsml_server/opsml_ui + /// This is only intended for development purposes. + #[arg(long = "dev-mode", default_value = "false")] + pub dev_mode: bool, } diff --git a/crates/opsml_cli/src/cli/commands.rs b/crates/opsml_cli/src/cli/commands.rs index 840028869..ccdcc0366 100644 --- a/crates/opsml_cli/src/cli/commands.rs +++ b/crates/opsml_cli/src/cli/commands.rs @@ -178,6 +178,7 @@ pub enum GenerateCommands { } #[derive(Subcommand)] +#[command(version = None)] pub enum ScouterCommands { /// Update Scouter Drift Profile Status /// @@ -187,6 +188,7 @@ pub enum ScouterCommands { } #[derive(Subcommand)] +#[command(version = None)] pub enum UiCommands { /// Start a local OpsML UI /// diff --git a/crates/opsml_cli/src/error.rs b/crates/opsml_cli/src/error.rs index 466b106c0..1ed8d86e0 100644 --- a/crates/opsml_cli/src/error.rs +++ b/crates/opsml_cli/src/error.rs @@ -98,6 +98,9 @@ pub enum UiError { #[error("Failed to get create cache directory")] CreateCacheDirError(#[source] std::io::Error), + #[error("Failed to get current directory")] + CurrentDirError(#[source] std::io::Error), + #[error("Unsupported platform - os: {0}, arch: {1}")] UnsupportedPlatformError(&'static str, &'static str), @@ -169,4 +172,16 @@ pub enum UiError { #[error("Script execution failed with status code: {0:?}")] ScriptFailedWithStatus(Option), + + #[error("Failed to start UI server")] + UiStartError, + + #[error("Failed to spawn UI process")] + UiSpawnError(#[source] std::io::Error), + + #[error("Node.js executable not found. Node is required to run the OpsML UI.")] + NodeNotFound, + + #[error("Package JSON not found")] + PackageJsonNotFound, } diff --git a/crates/opsml_cli/src/lib.rs b/crates/opsml_cli/src/lib.rs index ea934fd64..5b6b77af0 100644 --- a/crates/opsml_cli/src/lib.rs +++ b/crates/opsml_cli/src/lib.rs @@ -116,7 +116,8 @@ pub fn run_cli(args: Vec) -> anyhow::Result<()> { println!("Starting opsml-ui..."); let default_version = opsml_version::version(); let version = args.version.as_deref().unwrap_or_else(|| &default_version); - start_ui(version, None).context("Failed to start opsml-ui")?; + start_ui(version, &args.server_url, &args.ui_url, &args.dev_mode) + .context("Failed to start opsml-ui")?; Ok(()) } UiCommands::Stop => { diff --git a/crates/opsml_mocks/src/mock.rs b/crates/opsml_mocks/src/mock.rs index fbb716341..c072d44f5 100644 --- a/crates/opsml_mocks/src/mock.rs +++ b/crates/opsml_mocks/src/mock.rs @@ -212,10 +212,10 @@ impl OpsmlTestServer { println!("Mock Scouter Server stopped"); } - pub fn set_env_vars_for_client(&self) -> PyResult<()> { + pub fn set_env_vars_for_client(&self, _port: u16) -> PyResult<()> { #[cfg(feature = "server")] { - std::env::set_var("OPSML_TRACKING_URI", "http://localhost:3000"); + std::env::set_var("OPSML_TRACKING_URI", format!("http://localhost:{}", _port)); std::env::set_var("APP_ENV", "dev_client"); Ok(()) } @@ -245,7 +245,7 @@ impl OpsmlTestServer { let handle = self.handle.clone(); let runtime = self.runtime.clone(); - let port = match (3000..3010) + let port = match (8000..8010) .find(|port| StdTcpListener::bind(("127.0.0.1", *port)).is_ok()) { Some(p) => p, @@ -280,7 +280,7 @@ impl OpsmlTestServer { .send(); if let Ok(response) = res { if response.status() == 200 { - self.set_env_vars_for_client()?; + self.set_env_vars_for_client(port)?; println!("Opsml Server started successfully"); app_state().reset_app_state().map_err(|e| { TestServerError::CustomError(format!( diff --git a/crates/opsml_server/Cargo.toml b/crates/opsml_server/Cargo.toml index 348ba40bf..84fd32be4 100644 --- a/crates/opsml_server/Cargo.toml +++ b/crates/opsml_server/Cargo.toml @@ -34,7 +34,6 @@ mimalloc = { workspace = true } password-auth = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } -rust-embed = { workspace = true } rusty-logging = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/opsml_server/opsml_ui/package.json b/crates/opsml_server/opsml_ui/package.json index ccb994ac3..0d75f9925 100644 --- a/crates/opsml_server/opsml_ui/package.json +++ b/crates/opsml_server/opsml_ui/package.json @@ -4,14 +4,11 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "vite dev --host 0.0.0.0 --port 3000", "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint .", - "test": "vitest" + "preview": "vite preview --port 3000", + "start": "PORT=3000 node build/index.js", + "start:prod": "NODE_ENV=production PORT=3000 node build/index.js" }, "devDependencies": { "@eslint/compat": "^1.4.0", @@ -21,22 +18,23 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@skeletonlabs/skeleton": "3.0.0-next.12", "@skeletonlabs/skeleton-svelte": "1.0.0-next.21", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.46.2", + "@sveltejs/adapter-node": "^5.3.3", + "@sveltejs/kit": "^2.46.4", "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.14", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.2.8", + "@types/node": "^24.7.0", "eslint": "^9.37.0", "eslint-plugin-svelte": "^3.12.4", "globals": "^16.4.0", "jsdom": "^26.1.0", "mdsvex": "^0.12.6", "shiki": "^3.13.0", - "svelte": "^5.39.10", - "svelte-check": "^4.3.2", + "svelte": "^5.39.11", + "svelte-check": "^4.3.3", "svelte-fa": "^4.0.4", "tailwindcss": "^4.1.14", "typescript": "^5.9.3", @@ -70,6 +68,7 @@ "markdown-it": "^14.1.0", "marked": "^15.0.12", "marked-highlight": "^2.2.2", - "melt": "^0.40.2" + "melt": "^0.40.2", + "pino": "^10.0.0" } } \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/pnpm-lock.yaml b/crates/opsml_server/opsml_ui/pnpm-lock.yaml index c6cc2e2bf..cd23546ce 100644 --- a/crates/opsml_server/opsml_ui/pnpm-lock.yaml +++ b/crates/opsml_server/opsml_ui/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 11.11.1 lucide-svelte: specifier: ^0.477.0 - version: 0.477.0(svelte@5.39.10) + version: 0.477.0(svelte@5.39.11) markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -67,7 +67,10 @@ importers: version: 2.2.2(marked@15.0.12) melt: specifier: ^0.40.2 - version: 0.40.2(@floating-ui/dom@1.7.4)(svelte@5.39.10) + version: 0.40.2(@floating-ui/dom@1.7.4)(svelte@5.39.11) + pino: + specifier: ^10.0.0 + version: 10.0.0 devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -89,16 +92,16 @@ importers: version: 3.0.0-next.12(tailwindcss@4.1.14) '@skeletonlabs/skeleton-svelte': specifier: 1.0.0-next.21 - version: 1.0.0-next.21(svelte@5.39.10) - '@sveltejs/adapter-static': - specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.46.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))) + version: 1.0.0-next.21(svelte@5.39.11) + '@sveltejs/adapter-node': + specifier: ^5.3.3 + version: 5.3.3(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))) '@sveltejs/kit': - specifier: ^2.46.2 - version: 2.46.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + specifier: ^2.46.4 + version: 2.46.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + version: 5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.10(tailwindcss@4.1.14) @@ -107,19 +110,22 @@ importers: version: 0.5.19(tailwindcss@4.1.14) '@tailwindcss/vite': specifier: ^4.1.14 - version: 4.1.14(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + version: 4.1.14(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.8(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)) + version: 5.2.8(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4(@types/node@24.7.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)) + '@types/node': + specifier: ^24.7.0 + version: 24.7.0 eslint: specifier: ^9.37.0 version: 9.37.0(jiti@2.6.1) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.12.4(eslint@9.37.0(jiti@2.6.1))(svelte@5.39.10) + version: 3.12.4(eslint@9.37.0(jiti@2.6.1))(svelte@5.39.11) globals: specifier: ^16.4.0 version: 16.4.0 @@ -128,19 +134,19 @@ importers: version: 26.1.0 mdsvex: specifier: ^0.12.6 - version: 0.12.6(svelte@5.39.10) + version: 0.12.6(svelte@5.39.11) shiki: specifier: ^3.13.0 version: 3.13.0 svelte: - specifier: ^5.39.10 - version: 5.39.10 + specifier: ^5.39.11 + version: 5.39.11 svelte-check: - specifier: ^4.3.2 - version: 4.3.2(picomatch@4.0.3)(svelte@5.39.10)(typescript@5.9.3) + specifier: ^4.3.3 + version: 4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3) svelte-fa: specifier: ^4.0.4 - version: 4.0.4(svelte@5.39.10) + version: 4.0.4(svelte@5.39.11) tailwindcss: specifier: ^4.1.14 version: 4.1.14 @@ -152,10 +158,10 @@ importers: version: 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^6.3.6 - version: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + version: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) vitest: specifier: ^3.2.4 - version: 3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1) + version: 3.2.4(@types/node@24.7.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1) zod: specifier: ^3.25.76 version: 3.25.76 @@ -635,6 +641,42 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rollup/plugin-commonjs@28.0.6': + resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.2': + resolution: {integrity: sha512-tCtHJ2BlhSoK4cCs25NMXfV7EALKr0jyasmqVCq3y9cBrKdmJhtsy1iTz36Xhk/O+pDJbzawxF4K6ZblqCnITQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] @@ -787,13 +829,13 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-static@3.0.10': - resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} + '@sveltejs/adapter-node@5.3.3': + resolution: {integrity: sha512-SRDVuFBkmpKGsA9b0wYaCrrSChq2Yv5Dv8g7WiZcs8E69vdQNRamN0DzQV9/rEixvuRkojATLADNeQ+6FeyVNQ==} peerDependencies: - '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.46.2': - resolution: {integrity: sha512-bGs473Gj4TwFf7dw6ZUwQI0ayaDb83E7G06QnYeNQC2DmAaktgFU2uB0tSfZVhpHqYH4o8GsLBkG3ZjThtmsIA==} + '@sveltejs/kit@2.46.4': + resolution: {integrity: sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -968,6 +1010,12 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/node@24.7.0': + resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1065,106 +1113,106 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@zag-js/accordion@1.26.0': - resolution: {integrity: sha512-Sc7RfmGsCkYGBX5ksO4KqqjmAmNh6t/E0y4zkFjqJrUNYOIbEF0iqkni/j98NH9HG+nyNVu8nMOCD02bCJN9IA==} + '@zag-js/accordion@1.26.1': + resolution: {integrity: sha512-Rqp5zPyWn7w1D2teZAlLytK7okRjfdU4qLuwO7SPdXgeqxr+bn7qP9Bxs4NU78nySA8ZbLZqPbmTA31m9Ya6lw==} - '@zag-js/anatomy@1.26.0': - resolution: {integrity: sha512-6DUaClHkkBIL/PbM9D+vh7rCVyfFuBTGoKme5u5GBPa+elXWlk2R1i9r8O74wKiq84zDiEMYCertTpUaUi0OJw==} + '@zag-js/anatomy@1.26.1': + resolution: {integrity: sha512-1GVk5D/gFTvBd06w3MgDkSUGtUyU5n6XL8G6KsmuIh6dq16MgZ0TnDsUOkPVBQL3YEiHqlJfeCS6zkgcsu3q2g==} - '@zag-js/aria-hidden@1.26.0': - resolution: {integrity: sha512-aTr8WOhut0pWzYfPdqjK6ao+FLbOZSJjpzkZvcl+mtpL2UmQrDLyKgI2t6F4A48KbJPkPR2tkFcg1S32GcqIsw==} + '@zag-js/aria-hidden@1.26.1': + resolution: {integrity: sha512-8zZQDODCfXuJ0zfsx6+WVaYh+ScQoFG2ib2FvKr9sxztoJAn1hiLYFKzzlA1aWOxTAh+dFQC/jJI+C1nWbhBkQ==} - '@zag-js/auto-resize@1.26.0': - resolution: {integrity: sha512-xOAxLKg8PE6guq3QXeNFnlEef9PqlUnW1hTpGiClBz0CFGO5uCQtCaTJW0HKySSaq/qFiHFCbkP0V8sPNZL2bQ==} + '@zag-js/auto-resize@1.26.1': + resolution: {integrity: sha512-/t53ykgDOttYzroQQUgJqyOb+xlKClI5T1aJe/d7HOvGFqJtSSOYB9ZOJmywpkN9/11nYdH7HaZtSGmRjCQBzw==} - '@zag-js/avatar@1.26.0': - resolution: {integrity: sha512-7pyoMq/+4wqrFGX5LlhDlNNOigVHjdvmzUBcnSpDPTT+4vpy8La4TKX7JAHHEKa22DZWqptiC+mf3vZ17+ZaSw==} + '@zag-js/avatar@1.26.1': + resolution: {integrity: sha512-s34+KSqv/frY3W4Ng9uuuqQbVlgpeWG8b0CtGcELUw9VSldDSOE2ZMIZ1inFLoby9vOOovttHSd/QdGseXkFqQ==} - '@zag-js/collection@1.26.0': - resolution: {integrity: sha512-crynTfNJhTnhun6NgAQL+BcDU8ggWQnAS9uDkpFrf0MbamH3BM9KKGKF1VbJTIjnDeB1lA4RQ+w9GkGdvy/rKg==} + '@zag-js/collection@1.26.1': + resolution: {integrity: sha512-b23L4urNhCCH89TRr5UIWZsCtV9eaBhgTPu91qO2CAgz0fbvmkZJ/E5yr7EI4KeNg0xExbiQrhinZQt2v0LjCA==} - '@zag-js/combobox@1.26.0': - resolution: {integrity: sha512-eCGNozYy4HaHxENnMOOC7ArKXhxDc7ynOQ6XqemfUQWKHjFv4tOKYt7Zkiu6FErQ2o8zWLnRUnRmRRh+cQxF3g==} + '@zag-js/combobox@1.26.1': + resolution: {integrity: sha512-VLTTLvzgurvYrsg7UEb11Ad0ZPxWp3ui9/+J3rzgrTMEuLWj8mPfWT0i0l3AG+ruyFlQv5aZNvA/pupk3UK1Tg==} - '@zag-js/core@1.26.0': - resolution: {integrity: sha512-js9ut7V1n3Zhmp220PH/4oAS5qVW5ILSmSdZQor3I6JuDmSEiFr9yWTBELJCVOhwmYihE5e9uwdqfxIQsDegIQ==} + '@zag-js/core@1.26.1': + resolution: {integrity: sha512-JesW6C1dlrG36Aa+yteL0v5nt4Zqa9n9coqDJUQ9L1AYWzjz6NTHubsA7ysJlTKwl411gSsssmH9Ey5sRxFEWQ==} - '@zag-js/dialog@1.26.0': - resolution: {integrity: sha512-jIV7aOwiay9Kg54YpfqNtPObOeTBVkixJrbKz8wIebtz1ZPyYxMJM0Efz8Wpb8X2njx3U3g5c8RC10f6yHny5g==} + '@zag-js/dialog@1.26.1': + resolution: {integrity: sha512-kbNzTIMUkn19RcqDKF+3kH5cGPW3bY2FKiw6qtPi072SshHQtuAeE4g5K3wGFdSTJXHBhXYSK04ZR+ok00YbGA==} - '@zag-js/dismissable@1.26.0': - resolution: {integrity: sha512-xJJpotpmFs78fJL18yW/Fn0PQbqe7bAdw1Mm5iXQfCsPp1bWUSflA6gu0So/4NhyevavpkwAWRKm2Q520LAm9g==} + '@zag-js/dismissable@1.26.1': + resolution: {integrity: sha512-n7MdvInqfOh+UJ+VpeIovVU734vf8ekHhfhIugDt41xAzndZW8hdXcBsKnDFK0uY9OVk8cq5FRqpdzhoZuZSgw==} - '@zag-js/dom-query@1.26.0': - resolution: {integrity: sha512-T0shNXFZCCW2DmOWAiUStS8QmffLZhRaV0y3vXKllqqb61Bt6NRegen7ZJNSd9F+7SGpKAEmHjla6cRG0HC8Rg==} + '@zag-js/dom-query@1.26.1': + resolution: {integrity: sha512-AcdNV+Zn+Mrz6jq6IpgBJmiooFCuGBzhbbjjU6Drh3TfNDL7q2YXinrZydoD4EGDaFOQ1h71iChTtCZSHw/yNw==} - '@zag-js/file-upload@1.26.0': - resolution: {integrity: sha512-2B3HBSIfA23RK+byQpHSPub7qXu8rCp0m3G3wUGJBs506AkaPPbdv92QdHhlAsvgU3Ap2hanw+WGO4PUeioP7A==} + '@zag-js/file-upload@1.26.1': + resolution: {integrity: sha512-fu6UpK8+qj98g2h3XrFqWzaSD5JHNt71qEg6hlYCwsXdcpYhTObps9YsTKo4sd/XGVh0lriyzSIsjBUeoctR6A==} - '@zag-js/file-utils@1.26.0': - resolution: {integrity: sha512-YiAvR1ah7/ldezj3uKk+UgifKi9OC3DlVOFn80iTxuXLiLiWc3e4OLqWQdLhgybArRhJSL/U/O0Miu4nUiM4xQ==} + '@zag-js/file-utils@1.26.1': + resolution: {integrity: sha512-JW0vr7fzDDi9GEQLmGWSiBbNlSK8FQ/W+tavk0E4LNVb2Hs28/4F1XQXYKNuzfN+CoBYI74EIasb7rublozJMA==} - '@zag-js/focus-trap@1.26.0': - resolution: {integrity: sha512-XqCVLxeZBUS8+jFrBoihu+IFj60USGHvl9gLvN5OEzbIiTYc8Miun30Jq1H2vsGLvKh8Gxg5Di++TrpZxq1gbA==} + '@zag-js/focus-trap@1.26.1': + resolution: {integrity: sha512-88NvG1vxga/Umx54p9K6+nkrC5QLOA/hc5FV68eMKn83u1PCCH8U2+RV/vhMJ7IUTEwI0VTZLOmDfyfbkfomTQ==} - '@zag-js/focus-visible@1.26.0': - resolution: {integrity: sha512-4DcRtUkuv0CCuZd1S7i8J6AjNDKJGVEhDfFNq+VnZCg3G2ta+ou6Gc97TSgCQv1mJk23au/LJaPbDBhyJfcp3Q==} + '@zag-js/focus-visible@1.26.1': + resolution: {integrity: sha512-Hq57XwQWS0a5rjiZcgWhC8ca1e75VoqVLaSGn8XM29lv13423MPRtJ6b/zkEx5G4yZmddgA+wFh2/U2KPztcYA==} - '@zag-js/i18n-utils@1.26.0': - resolution: {integrity: sha512-EQqqNe0pnwzqtoIvz8ws6kdiCSjz3BtSZpq5pmkThrdTnu1K7Fd5jvP5nCJDaiWDVe0pQhQKAyIeySaYLwDb6A==} + '@zag-js/i18n-utils@1.26.1': + resolution: {integrity: sha512-bvzfdsiY4taqZA/QfgqofsynlJGoJGmtmV5zKcVdyTJRVQz04vSEIJ+bzEDpiqK0FtrOI2mOq3Ixmv8fKNRxlQ==} - '@zag-js/interact-outside@1.26.0': - resolution: {integrity: sha512-k1vXs/AHKwfFs7Eh+LbE7NqSU4AdN/L1SZGa/3wPwVt4d3QTgt4nY244ov3WQMuVFdBv138P85LH8Jca59xgaQ==} + '@zag-js/interact-outside@1.26.1': + resolution: {integrity: sha512-Kxyg8AuJq4WssVbFyCtY14O611DU7UEiKgDwiNBXdQ5V+XXd4BBBwrA5QK9pk/qjgxZawKOalwjcYsyWhFbwLQ==} - '@zag-js/live-region@1.26.0': - resolution: {integrity: sha512-4U4Ht9UhUCMDiVDj7lu3oXlFDqxagKA0cceyEwinLwhQjsDFf+3XSmZJ51x4kCO+T85G+8njFjEFzLOnHSL6DQ==} + '@zag-js/live-region@1.26.1': + resolution: {integrity: sha512-Z3c2/ssr5ERIVr0sLchua0e/JrPlbIN3suLZh4oYYPcxpKRjhwZQbN66xa5jm0LfIlW/Z21Obrb8X91GFwXR6w==} - '@zag-js/pagination@1.26.0': - resolution: {integrity: sha512-X4bYSqMepIqitesE2YPz7OvEIQL8HR8s4i7mqBXbjXB85vKFTbn68MPneKROu1Fl5JlR5b+sfA02P++S2rQK2A==} + '@zag-js/pagination@1.26.1': + resolution: {integrity: sha512-tInkqiMj1+VoFPSN80Y7lS1ABfxTA+xgyjMyx3Y09dgHNhiOKMOYva3eTGIJaEd6OskUayc3TFKqvfT5R43KlQ==} - '@zag-js/popover@1.26.0': - resolution: {integrity: sha512-KU1Y8p6jiCn1Wqi1O+vwf0fh7B6/fjDUqUIWzpqArRrW7tBhgyVBk99ryHGWR0g4I2F0X2vfq7M244dwKbuPng==} + '@zag-js/popover@1.26.1': + resolution: {integrity: sha512-nwZfOa7W8NW7npZXqw898nGIBJ7/sk89jglO9Ealqgay5ypOSGBg4v3Afc188Q9c4jFLLqNLQ/LzUhox+h7oLA==} - '@zag-js/popper@1.26.0': - resolution: {integrity: sha512-JglKZeXftTUjX3bC5W7wq4AbMaXN9aSnCrg8CMenM/ysTT0KM3K2EmLBYj0iKjwMRkHyMY225vunKgF1Qz4Gjg==} + '@zag-js/popper@1.26.1': + resolution: {integrity: sha512-wY/YzdXT97gIw8wpPDyU+qf1nlnf6wYwVs3SGDCd6cbgU7iy2jo3Pn6bMJ2OrsFrLZqFVZp6lF2zQtBTNJMZtg==} - '@zag-js/progress@1.26.0': - resolution: {integrity: sha512-UZlWDbULdcB55XFkV5x3zMZPBzQeDudwmMJBZFVV8h8jtX4Kb8MnComVjL/jR9UW3SkweiGh70hCC9VwBpY4hA==} + '@zag-js/progress@1.26.1': + resolution: {integrity: sha512-NEmLImi7ZvsTQ1B+AAZARsl3IwvdS4+qv9Wpgf+RIjJVCa0CbE3N7XS+sHuI5X4nWAKbP6Kpc3VKPwZ/9dZ7jQ==} - '@zag-js/radio-group@1.26.0': - resolution: {integrity: sha512-THWxa5nmkvrr4YArYMV9RRPzczhqJk/xUbw5MUvlqjVZK8EO9FJXWGbFxsBgap2DEIySps45gy5iFwNBbJomKQ==} + '@zag-js/radio-group@1.26.1': + resolution: {integrity: sha512-85BA4b4h6Dq3Qh9VhlRM49gJUk0NGnEqmd86LctNLXMTMeJQ9i40LMeRVlKJqeDDcXMPQDKsINCfATegq2mj2Q==} - '@zag-js/rating-group@1.26.0': - resolution: {integrity: sha512-g+9TzUo8b60ndkkG+zI+J4J/7WgPKuF57WeKIlmdy13kSelKOVaPgGF2jfY1cA4IuE8C5kZv9AhEFW4ET8o5KA==} + '@zag-js/rating-group@1.26.1': + resolution: {integrity: sha512-bhwqi7c9GtMC+ooO/BiP8NNY1RiMVYhkj6b18CCtkJHrkl+yOB3liqqE5sb7fuVe1A37S7JL+TmAySsRQLkC2g==} - '@zag-js/remove-scroll@1.26.0': - resolution: {integrity: sha512-940VAhNMNyrZWQQkYHzFcXmTJ79ldHoZptkaz6Vd0+xMBLPO8j7JECqiC37SezbOaCKjZnR8XFFAI3IgpLnogA==} + '@zag-js/remove-scroll@1.26.1': + resolution: {integrity: sha512-m1wVmykWfpvmp3Q3jLArCvojR8SM4/DTH6I5KIZAwgIp9QiI16K83ZTSS7TiVkhwt3SgH72F9oAaJT/HA9ZHnQ==} - '@zag-js/slider@1.26.0': - resolution: {integrity: sha512-gGV/C340PM6BhTI3Vk630XZHyZ2aD2+4G0C3eOrvZA9nWR85HPvYtgqQKPiNJf7KzG+xYXhnYFFWz4m8VDpa4A==} + '@zag-js/slider@1.26.1': + resolution: {integrity: sha512-iHFeugMk9pNZLoijEYeV49h7lLC+twxkpeiTc5KjFgKQB9n8a+H22fIVkl+B+dmIPcfr8tDpri1FfN4hh9kxpA==} - '@zag-js/svelte@1.26.0': - resolution: {integrity: sha512-jy+JufK1Z1IKuiDevKRfr4ldsRAz4Bq26YxTs4Ho/naP8EoSAFprNWa0gjmsR0VPsXkMg2Z5yjL/cjHQFOWGQQ==} + '@zag-js/svelte@1.26.1': + resolution: {integrity: sha512-v0xwiNYF5tqMR7rCmLz7N6Qy3zrFMfPuUoyyYgcd1XkLkjz5fUTZaco0mRTAYOVvJdbdnoQ0qa/Yek3VK27RCQ==} peerDependencies: svelte: '>=5' - '@zag-js/switch@1.26.0': - resolution: {integrity: sha512-f9aDc7Vr2TCydPdhAxl5hw+YuIwsGaQV6vvMtRpi8YCaYAN/PUasHwv6326ZjkEus2A4rH8gnbeO1BoebIPJJA==} + '@zag-js/switch@1.26.1': + resolution: {integrity: sha512-OfOF2BYBtEeoI4RU0eYTFH3QAORNQBuVtDrsERJuWldLk9p1sukETXooTFxqaQuLRx98J3Y7duryjvTvNyA8nA==} - '@zag-js/tabs@1.26.0': - resolution: {integrity: sha512-NV+LOg8ecQzGUz+L+frz6SS10EK3MXvabrq5ojoYxDRgDevriUB5Usr13VTnGlWUNLSigeb6plPFR+IF5pTEWw==} + '@zag-js/tabs@1.26.1': + resolution: {integrity: sha512-RI3Od0H6guYfflmCmpKO0hrh1FOjE/OduEEiD1XDn5foeD0uV/qhS8Wdh8R3ikgcaVjb5XrSOrCZXkuc732g8w==} - '@zag-js/tags-input@1.26.0': - resolution: {integrity: sha512-/pieT+fVL7k+mwj5DKaWRXUg0gj0Cu/0B0nXWOhE/QkQM8dGwirbq1INvf0Q3F7fWPKu2KbnrJbtUtltHen+yg==} + '@zag-js/tags-input@1.26.1': + resolution: {integrity: sha512-Gjjsq7CQolog1mEjU07hwnTPXQ9jjln35X52EjlsjOuohkzT0TFRDj5Nuf8fqu82k2OcSx8IX09wrdGgxMKklA==} - '@zag-js/tooltip@1.26.0': - resolution: {integrity: sha512-Pbs2T2NseOHsbQ8uHBwtCLpDTqbexwQxvbx1WtHnioQInPxGfrlbVAXbRttanhCzHuZVBWa6wQTUvFbtL6Dl1w==} + '@zag-js/tooltip@1.26.1': + resolution: {integrity: sha512-ezBF4DZYo7E+AjVvpOoXnoPwhSnbS3y6Fp9BMvzQRSn5qElZbdQOEZSI9R6MZBTfEo6syXE68MYyCMzKFpkQFg==} - '@zag-js/types@1.26.0': - resolution: {integrity: sha512-ONHss4vdveYhCxYD7mmWx/gOyzJeMdXmXSKuplzlSia7LZsPFGSRsXJ707TePOBqcbmxUghzgw19mVAS9KWRuA==} + '@zag-js/types@1.26.1': + resolution: {integrity: sha512-MXRn8x0af7ZVJ50cxCoJU7hLZ3FmPnD53UQnjkYSmOcwr1eE0Wyt7bjGUP1Zv+6hPMTlyZ18XR2rnUkKAtr7/w==} - '@zag-js/utils@1.26.0': - resolution: {integrity: sha512-xv4uahNYNzfRfrzDRh7iLy3OmO+nnq3Tg2IIFLnWsLqD+qC7z49x1vwyPqyRCvA7lXKp4FjV0e4b+V/shT4jTw==} + '@zag-js/utils@1.26.1': + resolution: {integrity: sha512-OeQia4UV/3osMdrhSa2NmhWvzlnlF1vxOBAdYP5R96ho0FpI15DvAaPQDvE94shzIMgzS4k0pqK5lZWK5FTmqw==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1209,6 +1257,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + axe-core@4.9.1: resolution: {integrity: sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==} engines: {node: '>=4'} @@ -1304,6 +1356,9 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1465,6 +1520,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1528,6 +1586,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + github-markdown-css@5.8.1: resolution: {integrity: sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==} engines: {node: '>=10'} @@ -1562,6 +1623,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1611,6 +1676,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1619,6 +1688,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1626,6 +1698,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -1892,6 +1967,10 @@ packages: nwsapi@2.2.22: resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -1925,6 +2004,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1943,6 +2025,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@10.0.0: + resolution: {integrity: sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==} + hasBin: true + postcss-load-config@3.1.4: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} @@ -1998,6 +2090,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -2012,6 +2107,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -2022,6 +2120,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -2039,6 +2141,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2063,6 +2170,10 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2096,6 +2207,12 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} + slow-redact@0.3.1: + resolution: {integrity: sha512-NvFvl1GuLZNW4U046Tfi8b26zXo8aBzgCAS2f7yVJR/fArN93mOqSA99cB9uITm92ajSz01bsu1K7SCVVjIMpQ==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2103,6 +2220,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2130,8 +2251,12 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - svelte-check@4.3.2: - resolution: {integrity: sha512-71udP5w2kaSTcX8iV0hn3o2FWlabQHhJTJLIQrCqMsrcOeDUO2VhCQKKCA8AMVHSPwdxLEWkUWh9OKxns5PD9w==} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.3.3: + resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -2152,8 +2277,8 @@ packages: peerDependencies: svelte: ^4.0.0 || ^5.0.0 - svelte@5.39.10: - resolution: {integrity: sha512-Q3gqCGIgl4r0CR7OaWYjVo22nqFmLLSfn1MiWNFaITamvqhGBD3kyqk51EKuO4Nd1z8QliO5KIz7a2Ka9Rxilw==} + svelte@5.39.11: + resolution: {integrity: sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==} engines: {node: '>=18'} symbol-tree@3.2.4: @@ -2173,6 +2298,9 @@ packages: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2246,6 +2374,9 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} @@ -3031,6 +3162,42 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@rollup/plugin-commonjs@28.0.6(rollup@4.52.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.52.4) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.19 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.4 + + '@rollup/plugin-json@6.1.0(rollup@4.52.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.52.4) + optionalDependencies: + rollup: 4.52.4 + + '@rollup/plugin-node-resolve@16.0.2(rollup@4.52.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.52.4) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 4.52.4 + + '@rollup/pluginutils@5.3.0(rollup@4.52.4)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.4 + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true @@ -3132,25 +3299,25 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@skeletonlabs/skeleton-svelte@1.0.0-next.21(svelte@5.39.10)': - dependencies: - '@zag-js/accordion': 1.26.0 - '@zag-js/avatar': 1.26.0 - '@zag-js/combobox': 1.26.0 - '@zag-js/dialog': 1.26.0 - '@zag-js/file-upload': 1.26.0 - '@zag-js/pagination': 1.26.0 - '@zag-js/popover': 1.26.0 - '@zag-js/progress': 1.26.0 - '@zag-js/radio-group': 1.26.0 - '@zag-js/rating-group': 1.26.0 - '@zag-js/slider': 1.26.0 - '@zag-js/svelte': 1.26.0(svelte@5.39.10) - '@zag-js/switch': 1.26.0 - '@zag-js/tabs': 1.26.0 - '@zag-js/tags-input': 1.26.0 - '@zag-js/tooltip': 1.26.0 - svelte: 5.39.10 + '@skeletonlabs/skeleton-svelte@1.0.0-next.21(svelte@5.39.11)': + dependencies: + '@zag-js/accordion': 1.26.1 + '@zag-js/avatar': 1.26.1 + '@zag-js/combobox': 1.26.1 + '@zag-js/dialog': 1.26.1 + '@zag-js/file-upload': 1.26.1 + '@zag-js/pagination': 1.26.1 + '@zag-js/popover': 1.26.1 + '@zag-js/progress': 1.26.1 + '@zag-js/radio-group': 1.26.1 + '@zag-js/rating-group': 1.26.1 + '@zag-js/slider': 1.26.1 + '@zag-js/svelte': 1.26.1(svelte@5.39.11) + '@zag-js/switch': 1.26.1 + '@zag-js/tabs': 1.26.1 + '@zag-js/tags-input': 1.26.1 + '@zag-js/tooltip': 1.26.1 + svelte: 5.39.11 '@skeletonlabs/skeleton@3.0.0-next.12(tailwindcss@4.1.14)': dependencies: @@ -3162,15 +3329,19 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.46.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))': + '@sveltejs/adapter-node@5.3.3(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.46.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.4) + '@rollup/plugin-json': 6.1.0(rollup@4.52.4) + '@rollup/plugin-node-resolve': 16.0.2(rollup@4.52.4) + '@sveltejs/kit': 2.46.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) + rollup: 4.52.4 - '@sveltejs/kit@2.46.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))': + '@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -3182,28 +3353,28 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.2 - svelte: 5.39.10 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + svelte: 5.39.11 + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) debug: 4.4.3 - svelte: 5.39.10 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + svelte: 5.39.11 + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.19 - svelte: 5.39.10 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) - vitefu: 1.1.1(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + svelte: 5.39.11 + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) + vitefu: 1.1.1(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) transitivePeerDependencies: - supports-color @@ -3281,12 +3452,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.14 - '@tailwindcss/vite@4.1.14(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.14(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.14 '@tailwindcss/oxide': 4.1.14 tailwindcss: 4.1.14 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) '@testing-library/dom@10.4.1': dependencies: @@ -3308,13 +3479,13 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.8(svelte@5.39.10)(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1))': + '@testing-library/svelte@5.2.8(svelte@5.39.11)(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4(@types/node@24.7.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1))': dependencies: '@testing-library/dom': 10.4.1 - svelte: 5.39.10 + svelte: 5.39.11 optionalDependencies: - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) - vitest: 3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) + vitest: 3.2.4(@types/node@24.7.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1) '@types/aria-query@5.0.4': {} @@ -3340,6 +3511,12 @@ snapshots: dependencies: '@types/unist': 2.0.11 + '@types/node@24.7.0': + dependencies: + undici-types: 7.14.0 + + '@types/resolve@1.20.2': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -3447,13 +3624,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1))': + '@vitest/mocker@3.2.4(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -3481,222 +3658,222 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@zag-js/accordion@1.26.0': + '@zag-js/accordion@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/anatomy@1.26.0': {} + '@zag-js/anatomy@1.26.1': {} - '@zag-js/aria-hidden@1.26.0': + '@zag-js/aria-hidden@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/auto-resize@1.26.0': + '@zag-js/auto-resize@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/avatar@1.26.0': + '@zag-js/avatar@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/collection@1.26.0': + '@zag-js/collection@1.26.1': dependencies: - '@zag-js/utils': 1.26.0 + '@zag-js/utils': 1.26.1 - '@zag-js/combobox@1.26.0': + '@zag-js/combobox@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/aria-hidden': 1.26.0 - '@zag-js/collection': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dismissable': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/popper': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/collection': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/core@1.26.0': + '@zag-js/core@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/dialog@1.26.0': + '@zag-js/dialog@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/aria-hidden': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dismissable': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/focus-trap': 1.26.0 - '@zag-js/remove-scroll': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-trap': 1.26.1 + '@zag-js/remove-scroll': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/dismissable@1.26.0': + '@zag-js/dismissable@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 - '@zag-js/interact-outside': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/dom-query': 1.26.1 + '@zag-js/interact-outside': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/dom-query@1.26.0': + '@zag-js/dom-query@1.26.1': dependencies: - '@zag-js/types': 1.26.0 + '@zag-js/types': 1.26.1 - '@zag-js/file-upload@1.26.0': + '@zag-js/file-upload@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/file-utils': 1.26.0 - '@zag-js/i18n-utils': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/file-utils': 1.26.1 + '@zag-js/i18n-utils': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/file-utils@1.26.0': + '@zag-js/file-utils@1.26.1': dependencies: - '@zag-js/i18n-utils': 1.26.0 + '@zag-js/i18n-utils': 1.26.1 - '@zag-js/focus-trap@1.26.0': + '@zag-js/focus-trap@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/focus-visible@1.26.0': + '@zag-js/focus-visible@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/i18n-utils@1.26.0': + '@zag-js/i18n-utils@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/interact-outside@1.26.0': + '@zag-js/interact-outside@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/live-region@1.26.0': {} + '@zag-js/live-region@1.26.1': {} - '@zag-js/pagination@1.26.0': + '@zag-js/pagination@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/popover@1.26.0': + '@zag-js/popover@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/aria-hidden': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dismissable': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/focus-trap': 1.26.0 - '@zag-js/popper': 1.26.0 - '@zag-js/remove-scroll': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-trap': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/remove-scroll': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/popper@1.26.0': + '@zag-js/popper@1.26.1': dependencies: '@floating-ui/dom': 1.7.4 - '@zag-js/dom-query': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/progress@1.26.0': + '@zag-js/progress@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/radio-group@1.26.0': + '@zag-js/radio-group@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/focus-visible': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/rating-group@1.26.0': + '@zag-js/rating-group@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/remove-scroll@1.26.0': + '@zag-js/remove-scroll@1.26.1': dependencies: - '@zag-js/dom-query': 1.26.0 + '@zag-js/dom-query': 1.26.1 - '@zag-js/slider@1.26.0': + '@zag-js/slider@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/svelte@1.26.0(svelte@5.39.10)': + '@zag-js/svelte@1.26.1(svelte@5.39.11)': dependencies: - '@zag-js/core': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 - svelte: 5.39.10 + '@zag-js/core': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + svelte: 5.39.11 - '@zag-js/switch@1.26.0': + '@zag-js/switch@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/focus-visible': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/tabs@1.26.0': + '@zag-js/tabs@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/tags-input@1.26.0': + '@zag-js/tags-input@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/auto-resize': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/interact-outside': 1.26.0 - '@zag-js/live-region': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/auto-resize': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/interact-outside': 1.26.1 + '@zag-js/live-region': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/tooltip@1.26.0': + '@zag-js/tooltip@1.26.1': dependencies: - '@zag-js/anatomy': 1.26.0 - '@zag-js/core': 1.26.0 - '@zag-js/dom-query': 1.26.0 - '@zag-js/focus-visible': 1.26.0 - '@zag-js/popper': 1.26.0 - '@zag-js/types': 1.26.0 - '@zag-js/utils': 1.26.0 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 - '@zag-js/types@1.26.0': + '@zag-js/types@1.26.1': dependencies: csstype: 3.1.3 - '@zag-js/utils@1.26.0': {} + '@zag-js/utils@1.26.1': {} acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -3731,6 +3908,8 @@ snapshots: assertion-error@2.0.1: {} + atomic-sleep@1.0.0: {} + axe-core@4.9.1: {} axobject-query@4.1.0: {} @@ -3820,6 +3999,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commondir@1.0.1: {} + concat-map@0.0.1: {} cookie@0.6.0: {} @@ -3920,7 +4101,7 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-svelte@3.12.4(eslint@9.37.0(jiti@2.6.1))(svelte@5.39.10): + eslint-plugin-svelte@3.12.4(eslint@9.37.0(jiti@2.6.1))(svelte@5.39.11): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -3932,9 +4113,9 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.3.3(svelte@5.39.10) + svelte-eslint-parser: 1.3.3(svelte@5.39.11) optionalDependencies: - svelte: 5.39.10 + svelte: 5.39.11 transitivePeerDependencies: - ts-node @@ -4011,6 +4192,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -4068,6 +4251,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + github-markdown-css@5.8.1: {} glob-parent@5.1.2: @@ -4090,6 +4275,10 @@ snapshots: has-flag@4.0.0: {} + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -4147,16 +4336,26 @@ snapshots: indent-string@4.0.0: {} + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-module@1.0.0: {} + is-number@7.0.0: {} is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -4305,9 +4504,9 @@ snapshots: lru-cache@10.4.3: {} - lucide-svelte@0.477.0(svelte@5.39.10): + lucide-svelte@0.477.0(svelte@5.39.11): dependencies: - svelte: 5.39.10 + svelte: 5.39.11 lz-string@1.5.0: {} @@ -4342,26 +4541,26 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 - mdsvex@0.12.6(svelte@5.39.10): + mdsvex@0.12.6(svelte@5.39.11): dependencies: '@types/mdast': 4.0.4 '@types/unist': 2.0.11 prism-svelte: 0.4.7 prismjs: 1.30.0 - svelte: 5.39.10 + svelte: 5.39.11 unist-util-visit: 2.0.3 vfile-message: 2.0.4 mdurl@2.0.0: {} - melt@0.40.2(@floating-ui/dom@1.7.4)(svelte@5.39.10): + melt@0.40.2(@floating-ui/dom@1.7.4)(svelte@5.39.11): dependencies: '@floating-ui/dom': 1.7.4 dequal: 2.0.3 focus-trap: 7.6.5 jest-axe: 9.0.0 - runed: 0.23.4(svelte@5.39.10) - svelte: 5.39.10 + runed: 0.23.4(svelte@5.39.11) + svelte: 5.39.11 merge2@1.4.1: {} @@ -4417,6 +4616,8 @@ snapshots: nwsapi@2.2.22: {} + on-exit-leak-free@2.1.2: {} + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.3: @@ -4454,6 +4655,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -4464,6 +4667,26 @@ snapshots: picomatch@4.0.3: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@10.0.0: + dependencies: + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + slow-redact: 0.3.1 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + postcss-load-config@3.1.4(postcss@8.5.6): dependencies: lilconfig: 2.1.0 @@ -4513,6 +4736,8 @@ snapshots: prismjs@1.30.0: {} + process-warning@5.0.0: {} + property-information@7.1.0: {} punycode.js@2.3.1: {} @@ -4521,12 +4746,16 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + react-is@17.0.2: {} react-is@18.3.1: {} readdirp@4.1.2: {} + real-require@0.2.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -4544,6 +4773,12 @@ snapshots: resolve-from@4.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} rollup@4.52.4: @@ -4580,15 +4815,17 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.23.4(svelte@5.39.10): + runed@0.23.4(svelte@5.39.11): dependencies: esm-env: 1.2.2 - svelte: 5.39.10 + svelte: 5.39.11 sade@1.8.1: dependencies: mri: 1.2.0 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -4624,10 +4861,18 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + slow-redact@0.3.1: {} + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + stackback@0.0.2: {} std-env@3.9.0: {} @@ -4653,19 +4898,21 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.2(picomatch@4.0.3)(svelte@5.39.10)(typescript@5.9.3): + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.39.10 + svelte: 5.39.11 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.3.3(svelte@5.39.10): + svelte-eslint-parser@1.3.3(svelte@5.39.11): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -4674,13 +4921,13 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 optionalDependencies: - svelte: 5.39.10 + svelte: 5.39.11 - svelte-fa@4.0.4(svelte@5.39.10): + svelte-fa@4.0.4(svelte@5.39.11): dependencies: - svelte: 5.39.10 + svelte: 5.39.11 - svelte@5.39.10: + svelte@5.39.11: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -4713,6 +4960,10 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4773,6 +5024,8 @@ snapshots: uc.micro@2.1.0: {} + undici-types@7.14.0: {} + unist-util-is@4.1.0: {} unist-util-is@6.0.0: @@ -4834,13 +5087,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(jiti@2.6.1)(lightningcss@1.30.1): + vite-node@3.2.4(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4855,7 +5108,7 @@ snapshots: - tsx - yaml - vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1): + vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -4864,19 +5117,20 @@ snapshots: rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.7.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.1 - vitefu@1.1.1(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)): + vitefu@1.1.1(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)): optionalDependencies: - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) - vitest@3.2.4(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1): + vitest@3.2.4(@types/node@24.7.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.6(jiti@2.6.1)(lightningcss@1.30.1)) + '@vitest/mocker': 3.2.4(vite@6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4894,10 +5148,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.6(jiti@2.6.1)(lightningcss@1.30.1) - vite-node: 3.2.4(jiti@2.6.1)(lightningcss@1.30.1) + vite: 6.3.6(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) + vite-node: 3.2.4(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 24.7.0 jsdom: 26.1.0 transitivePeerDependencies: - jiti diff --git a/crates/opsml_server/opsml_ui/site/.gitkeep b/crates/opsml_server/opsml_ui/site/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/opsml_server/opsml_ui/src/hooks.server.ts b/crates/opsml_server/opsml_ui/src/hooks.server.ts new file mode 100644 index 000000000..6e5abeb4c --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/hooks.server.ts @@ -0,0 +1,54 @@ +import { validateTokenOrRedirect } from "$lib/server/auth/validateToken"; +import type { Handle } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger"; +import { ServerPaths, UiPaths } from "$lib/components/api/routes"; +import type { HandleFetch } from "@sveltejs/kit"; + +// These routes do not require authentication +const PUBLIC_ROUTES = [ + UiPaths.LOGIN, + UiPaths.REGISTER, + UiPaths.RESET, + UiPaths.FORGOT, + UiPaths.SSO_AUTH, + UiPaths.SSO_CALLBACK, + ServerPaths.LOGIN, + ServerPaths.REGISTER_USER, + ServerPaths.RESET_PASSWORD, + ServerPaths.SSO_AUTH, + ServerPaths.SSO_CALLBACK, + ServerPaths.USER, + ServerPaths.HEALTHCHECK, + "/", +]; + +export const handle: Handle = async ({ event, resolve }) => { + logger.debug(`Handling request for: ${event.url.pathname}`); + + if (!PUBLIC_ROUTES.includes(event.url.pathname)) { + try { + await validateTokenOrRedirect(event.cookies); + logger.debug(`User authenticated successfully.`); + } catch (err) { + if (err instanceof Response) return err; + throw err; + } + } + logger.debug( + `Request for ${event.url.pathname} passed authentication check.` + ); + + return await resolve(event); +}; + +export const handleFetch: HandleFetch = async ({ request, event, fetch }) => { + const token = event.cookies.get("jwt_token"); + + if (token) { + // add the auth token to all outgoing requests: + // - Requests are either being routed to the server endpoints or backend api + request.headers.set("Authorization", `Bearer ${token}`); + } + + return fetch(request); +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/api/internalClient.ts b/crates/opsml_server/opsml_ui/src/lib/api/internalClient.ts new file mode 100644 index 000000000..3932d5ecb --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/api/internalClient.ts @@ -0,0 +1,143 @@ +import { redirect } from "@sveltejs/kit"; + +/** + * HTTP client for SvelteKit server endpoints. + * Uses relative paths and idiomatic error handling. + */ +export class InternalApiClient { + private fetchFn: typeof fetch; + + constructor(fetchFn: typeof fetch = fetch) { + this.fetchFn = fetchFn; + } + + /** + * Handles API errors and redirects on 401. + * @param error - Error object or message + * @param status - Optional HTTP status code + */ + private handleError(error: unknown, status?: number): void { + console.error("Server API Error:", error); + if (status === 401) { + throw redirect(303, "/opsml/user/login"); + } + } + + /** + * Adds query parameters to a URL object. + * Supports arrays and objects for compatibility. + */ + private addQueryParams(url: URL, params?: Record): URL { + if (!params) return url; + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if ( + Array.isArray(value) || + (typeof value === "object" && typeof value.length === "number") + ) { + for (let i = 0; i < value.length; i++) { + url.searchParams.append(`${key}[${i}]`, String(value[i])); + } + } else if (typeof value === "object") { + url.searchParams.append(key, JSON.stringify(value)); + } else { + url.searchParams.append(key, String(value)); + } + } + }); + return url; + } + + /** + * Generic request handler for all HTTP methods. + * @param path - Relative API path + * @param options - Fetch options + */ + async request(path: string, options: RequestInit = {}): Promise { + const url = new URL(path, "http://localhost"); // base is ignored for relative paths + try { + const response = await this.fetchFn(url.pathname + url.search, { + ...options, + headers: options.headers, + }); + + if (!response.ok) { + this.handleError(`HTTP ${response.status}`, response.status); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response; + } catch (error) { + this.handleError(error); + throw error; + } + } + + /** + * GET request with optional query params. + */ + async get(path: string, params?: Record): Promise { + const url = this.addQueryParams(new URL(path, "http://localhost"), params); + const headers: Record = {}; + return this.request(url.pathname + url.search, { method: "GET", headers }); + } + + /** + * DELETE request with optional body and bearer token. + */ + async delete(path: string, body?: any, token?: string): Promise { + const headers: Record = {}; + return this.request(path, { + method: "DELETE", + headers: { "Content-Type": "application/json", ...headers }, + body: body ? JSON.stringify(body) : undefined, + }); + } + + async put(path: string, body: any): Promise { + const headers: Record = {}; + return this.request(path, { + method: "PUT", + headers: { "Content-Type": "application/json", ...headers }, + body: JSON.stringify(body), + }); + } + + /** + * PATCH request with body and optional bearer token. + */ + async patch(path: string, body: any): Promise { + const headers: Record = {}; + return this.request(path, { + method: "PATCH", + headers: { "Content-Type": "application/json", ...headers }, + body: JSON.stringify(body), + }); + } + + /** + * POST request with body and optional bearer token. + */ + async post( + path: string, + body: any, + additionalHeaders: Record = {} + ): Promise { + return this.request(path, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...additionalHeaders, + }, + body: JSON.stringify(body), + }); + } +} + +/** Singleton instance for usage in SvelteKit server code */ +export const internalApiClient = new InternalApiClient(); +export function createInternalApiClient( + fetchFn: typeof fetch = fetch +): InternalApiClient { + return new InternalApiClient(fetchFn); +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/api/client.svelte.ts b/crates/opsml_server/opsml_ui/src/lib/components/api/client.svelte.ts deleted file mode 100644 index 68aa9ccc6..000000000 --- a/crates/opsml_server/opsml_ui/src/lib/components/api/client.svelte.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { goto } from "$app/navigation"; -import { UiPaths } from "$lib/components/api/routes"; -import { browser } from "$app/environment"; -import Console from "../card/monitoring/update/dispatch/Console.svelte"; - -export class OpsmlClient { - // UserStore functionality as class properties with runes - // user = $state(userStore); - - constructor() { - if (browser) { - // start active user session - // This will load any stored token from the cookie - //this.user = userStore; - //if (this.user.jwt_token !== "") { - // this.validateAuth(); - //} - } - } - - //// API handler methods - private async handleError(response: Response): Promise { - const errorMessage = await response.text(); - void goto(`${UiPaths.ERROR}?message=${errorMessage}`); - return new Response(null, { status: 500, statusText: "Failure" }); - } - - private addQueryParams(url: string, params?: Record): string { - if (!params) return url; - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - if ( - Array.isArray(value) || - (typeof value === "object" && typeof value.length === "number") - ) { - for (let i = 0; i < value.length; i++) { - searchParams.append(`${key}[${i}]`, String(value[i])); - } - } else if (typeof value === "object") { - searchParams.append(key, JSON.stringify(value)); - } else { - searchParams.append(key, String(value)); - } - } - }); - const queryString = searchParams.toString(); - return queryString ? `${url}?${queryString}` : url; - } - - async request( - url: string, - method: string, - body: any = null, - bearerToken: string, - contentType: string = "application/json", - additionalHeaders: Record = {} - ): Promise { - const userAgent = browser ? navigator.userAgent : "opsml-ui"; - - const headers = { - "Content-Type": contentType, - "User-Agent": userAgent, - Authorization: `Bearer ${bearerToken}`, - ...additionalHeaders, - }; - - if (browser) { - const response = await fetch(url, { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - }); - - return response.ok ? response : await this.handleError(response); - } - - return new Response(null, { status: 500, statusText: "Failure" }); - } - - async get( - url: string, - params?: Record, - bearerToken: string = "" - ): Promise { - const urlWithParams = this.addQueryParams(url, params); - - return this.request(urlWithParams, "GET", null, bearerToken); - } - - async delete( - url: string, - body: any = null, - bearerToken: string = "", - contentType: string = "application/json" - ): Promise { - return this.request(url, "DELETE", body, bearerToken, contentType); - } - - async put( - url: string, - body: any, - bearerToken: string = "", - contentType: string = "application/json" - ): Promise { - return this.request(url, "PUT", body, bearerToken, contentType); - } - - async patch( - url: string, - body: any, - bearerToken: string = "", - contentType: string = "application/json" - ): Promise { - return this.request(url, "PATCH", body, bearerToken, contentType); - } - - async post( - url: string, - body: any, - bearerToken: string = "", - contentType: string = "application/json", - additionalHeaders: Record = {} - ): Promise { - return this.request( - url, - "POST", - body, - bearerToken, - contentType, - additionalHeaders - ); - } -} - -// Create and export a singleton instance -export const opsmlClient = new OpsmlClient(); diff --git a/crates/opsml_server/opsml_ui/src/lib/components/api/registry.ts b/crates/opsml_server/opsml_ui/src/lib/components/api/registry.ts new file mode 100644 index 000000000..d2a386bfe --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/api/registry.ts @@ -0,0 +1,51 @@ +import { createInternalApiClient } from "$lib/api/internalClient"; +import type { + RegistryStatsResponse, + QueryPageResponse, +} from "$lib/components/card/types"; +import { RegistryType } from "$lib/utils"; +import { ServerPaths } from "$lib/components/api/routes"; + +/** Helper function to call the server registry page endpoint */ +export async function getRegistryPage( + fetch: typeof globalThis.fetch, + registry_type: RegistryType, + sort_by: string | undefined, + spaces: string[] | undefined, + searchTerm: string | undefined, + tags: string[] | undefined, + page: number +): Promise { + let resp = await createInternalApiClient(fetch).post( + ServerPaths.REGISTRY_PAGE, + { + registry_type, + sort_by, + spaces, + searchTerm, + tags, + page, + } + ); + return (await resp.json()) as QueryPageResponse; +} + +/** Helper function to call the server registry stats endpoint */ +export async function getRegistryStats( + fetch: typeof globalThis.fetch, + registry_type: RegistryType, + searchTerm: string | undefined, + spaces: string[] | undefined, + tags: string[] | undefined +): Promise { + let resp = await createInternalApiClient(fetch).post( + ServerPaths.REGISTRY_STATS, + { + registry_type, + searchTerm, + spaces, + tags, + } + ); + return (await resp.json()) as RegistryStatsResponse; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/api/routes.ts b/crates/opsml_server/opsml_ui/src/lib/components/api/routes.ts index ef92f90e5..8e5b81a1d 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/api/routes.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/api/routes.ts @@ -57,7 +57,10 @@ export enum UiPaths { LOGIN = "/opsml/user/login", REGISTER = "/opsml/user/register", REGISTER_SUCCESS = "/opsml/user/register/success", + RESET = "/opsml/user/reset", FORGOT = "/opsml/user/forgot", + SSO_AUTH = "/opsml/user/sso", + SSO_CALLBACK = "/opsml/user/sso/callback", USER = "/opsml/user", HOME = "/opsml/home", METRICS = "/opsml/metrics", @@ -65,3 +68,32 @@ export enum UiPaths { EXPERIMENT = "/opsml/experiment", ERROR = "/opsml/error", } + +export enum ServerPaths { + LOGIN = "/api/user/login", + LOGOUT = "/api/user/logout", + RESET_PASSWORD = "/api/user/reset", + REGISTER_USER = "/api/user/register", + SSO_AUTH = "/api/user/sso", + SSO_CALLBACK = "/api/user/sso/callback", + USER = "/api/user/info", + UPDATE_USER = "/api/user/update", + REGISTRY_PAGE = "/api/card/registry/page", + REGISTRY_STATS = "/api/card/registry/stats", + METADATA = "/api/card/metadata", + CARD_FROM_UID = "/api/card/uid", + VERSION_PAGE = "/api/card/registry/version/page", + MONITORING_METRICS = "/api/card/monitoring/metrics", + MONITORING_PROFILES = "/api/card/monitoring/profiles", + MONITORING_ALERTS = "/api/card/monitoring/alerts", + ACKNOWLEDGE_ALERT = "/api/card/monitoring/alerts/acknowledge", + UPDATE_MONITORING_PROFILE = "/api/card/monitoring/profile/update", + LLM_MONITORING_RECORDS = "/api/card/monitoring/llm/records", + EXPERIMENT_GROUPED_METRICS = "/api/card/experiment/metrics/grouped", + DATA_PROFILE = "/api/card/data/profile", + EXPERIMENT_METRIC_NAMES = "/api/card/experiment/metrics/names", + EXPERIMENT_RECENT = "/api/card/experiment/recent", + EXPERIMENT_HARDWARE = "/api/card/experiment/hardware", + CREATE_SPACE = "/api/space/create", + HEALTHCHECK = "/api/health", +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/CardReadMe.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/CardReadMe.svelte index 9fe6b133b..9fe1b1234 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/CardReadMe.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/CardReadMe.svelte @@ -22,20 +22,12 @@ readMe: ReadMe; }>(); - - - - let readMeUrl = $state(`/opsml/${getRegistryPath(registryType)}/card/${space}/${name}/${version}/readme`); - - onMount(async () => { - if (readMe.exists) { html = await convertMarkdown(readMe.readme); } - }); diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/CardSearch.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/CardSearch.svelte index de16924fc..84bf5515d 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/CardSearch.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/CardSearch.svelte @@ -3,14 +3,15 @@ import { Switch } from '@skeletonlabs/skeleton-svelte'; import CardTableView from "$lib/components/card/CardTableView.svelte"; import { onMount } from "svelte"; - import { getRegistryPage, getRegistryStats} from "$lib/components/card/utils"; import type { RegistryPageReturn, RegistryStatsResponse, QueryPageResponse} from "$lib/components/card/types"; import { RegistryType, delay, getRegistryTypeUpperCase } from "$lib/utils"; - import { ArrowLeft, ArrowRight, Search, Settings } from 'lucide-svelte'; + import { ArrowLeft, ArrowRight, Settings } from 'lucide-svelte'; + import { getRegistryPage, getRegistryStats } from '$lib/components/api/registry'; import { Combobox } from "melt/builders"; import CardPage from '$lib/components/card/CardPage.svelte'; + import MultiComboBoxDropDown from '$lib/components/utils/MultiComboBoxDropDown.svelte'; - let { page, selectedName, selectedSpace} = $props<{ + let { page, selectedName, selectedSpace } = $props<{ page: RegistryPageReturn; selectedName: string | undefined; selectedSpace: string | undefined; @@ -20,6 +21,7 @@ let currentPage = $state(1); let totalPages = $state(1); let artifactSearchQuery = $state(selectedName || ''); + let filteredSpaces: string[] = $state([]); let filteredTags: string[] = $state([]); @@ -32,10 +34,6 @@ let availableSpaces = page.spaces; let availableTags = page.tags; - //@ts-ignore - const spacesCombobox = new Combobox({ multiple: true, onValueChange: onSpaceChange }); - //@ts-ignore - const tagsCombobox = new Combobox({ multiple: true, onValueChange: onTagsChange }); onMount(() => { totalPages = Math.ceil(registryStats.stats.nbr_names / 30); @@ -43,69 +41,42 @@ // if selectedSpace is defined, add it to filteredSpaces and spacesCombobox if (selectedSpace && !filteredSpaces.includes(selectedSpace)) { filteredSpaces = [...filteredSpaces, selectedSpace]; - spacesCombobox.select(selectedSpace); } }); - let searchTimeout: ReturnType | null = null; - function onTagsChange() { + + function onInputChange() { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(async () => { - // set filteredTags based on tagsCombobox.value - //@ts-ignore - filteredTags = [...tagsCombobox.value] as string[]; await searchPage(); }, 100); } - function onSpaceChange() { - if (searchTimeout) clearTimeout(searchTimeout); - searchTimeout = setTimeout(async () => { - // set filteredSpaces based on spacesCombobox.value - //@ts-ignore - filteredSpaces = [...spacesCombobox.value] as string[]; - await searchPage(); - }, 100); - } + // effect to run SearchPage when filteredSpaces changes + $effect(() => { + if (filteredSpaces.length > 0 || filteredTags.length > 0) { + onInputChange(); + } + }); const searchPage = async function () { [registryPage, registryStats] = await Promise.all([ - getRegistryPage(registryType, undefined, filteredSpaces, artifactSearchQuery, filteredTags, 1), - getRegistryStats(registryType, artifactSearchQuery, filteredSpaces, filteredTags) + getRegistryPage(fetch, registryType, undefined, filteredSpaces, artifactSearchQuery, filteredTags, 1), + getRegistryStats(fetch, registryType, artifactSearchQuery, filteredSpaces, filteredTags) ]); currentPage = 1; totalPages = Math.ceil(registryStats.stats.nbr_names / 30); } const changePage = async function (page: number) { - registryPage = await getRegistryPage(registryType, undefined, filteredSpaces, artifactSearchQuery, filteredTags, page); + registryPage = await getRegistryPage(fetch, registryType, undefined, filteredSpaces, artifactSearchQuery, filteredTags, page); currentPage = page; } - @@ -126,77 +97,24 @@
-
-
- - -
-
- {#each availableSpaces as option (option)} -
- {option} - {#if spacesCombobox.isSelected(option)} - ✓ - {/if} -
- {:else} - No results found - {/each} -
- - -
- {#each filteredSpaces as space} - - {/each} -
+ -

-
-
- - -
-
- {#each availableTags as option (option)} -
- {option} - {#if tagsCombobox.isSelected(option)} - ✓ - {/if} -
- {:else} - No results found - {/each} -
- -
- {#each filteredTags as tag} - - {/each} -
-
+ +
+
diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/VersionPage.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/VersionPage.svelte index 8314ae8fb..d61ea36af 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/VersionPage.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/VersionPage.svelte @@ -1,12 +1,13 @@
-
-
{#if data.readme.exists}
@@ -37,11 +33,9 @@
{/if}
- -
-
-
- - - - \ No newline at end of file +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/data/DataProfileViz.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/data/DataProfileViz.svelte index 22fab458e..031d07341 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/data/DataProfileViz.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/data/DataProfileViz.svelte @@ -1,10 +1,10 @@ -
-
-
-
Data Profile:
-
-
- +
+ +
+
+
+
+
- - +
-
- {#each features as feature} - {@const featureProfile: FeatureProfile = profile.features[feature]} -
-
- - - {#if !featureProfile.numeric_stats} - - {:else} - - {/if} -
- {#if featureProfile.numeric_stats} - - {:else if featureProfile.string_stats} - - {/if} + +
+ {#each filteredFeatures as feature} + {@const featureProfile: FeatureProfile = profile.features[feature]} +
+ +
+ + +
+ + + {#if featureProfile.numeric_stats} + + {:else if featureProfile.string_stats} + + {/if} +
{/each} -
-
03
-
+
+
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/data/NumericStats.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/data/NumericStats.svelte index 9143c6d60..66b54fb6d 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/data/NumericStats.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/data/NumericStats.svelte @@ -2,90 +2,83 @@ import type { NumericStats } from "./types"; import HistChart from "$lib/components/viz/HistChart.svelte"; - -let { - numericData -} = $props<{ - numericData: NumericStats; -}>(); - + let { numericData } = $props<{ numericData: NumericStats }>(); let resetZoom: boolean = $state(false); - - let resetZoomClicked = () => { + + function resetZoomClicked() { resetZoom = !resetZoom; } - -
-
-
- -
-
-
General Stats
-
-
-
Distinct:
-
{numericData.distinct.count} ({numericData.distinct.percent}%)
-
-
-
Mean:
-
{numericData.stddev.toFixed(3)}
-
-
-
Minimum:
-
{numericData.min.toFixed(3)}
-
-
-
Maximum:
-
{numericData.max.toFixed(3)}
-
+
+ +
+ +
+
+
General Stats
- - -
-
-
Quantiles
-
-
-
Q25:
-
{numericData.quantiles.q25.toFixed(3)}
-
-
-
Q50:
-
{numericData.quantiles.q50.toFixed(3)}
-
-
-
Q75:
-
{numericData.quantiles.q75.toFixed(3)}
-
-
-
Q99:
-
{numericData.quantiles.q99.toFixed(3)}
-
+
+
Distinct:
+
{numericData.distinct.count} ({numericData.distinct.percent}%)
+
+
+
Mean:
+
{numericData.stddev.toFixed(3)}
+
+
+
Minimum:
+
{numericData.min.toFixed(3)}
+
+
+
Maximum:
+
{numericData.max.toFixed(3)}
+ +
+
+
Quantiles
+
+
+
Q25:
+
{numericData.quantiles.q25.toFixed(3)}
+
+
+
Q50:
+
{numericData.quantiles.q50.toFixed(3)}
+
+
+
Q75:
+
{numericData.quantiles.q75.toFixed(3)}
+
+
+
Q99:
+
{numericData.quantiles.q99.toFixed(3)}
+
+
-
+ +
-
-
Distribution
-
-
- -
- +
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/data/StringStats.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/data/StringStats.svelte index 08688ab75..85be37f0e 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/data/StringStats.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/data/StringStats.svelte @@ -1,73 +1,64 @@ - -
-
-
- - -
-
-
Stats
-
-
-
Distinct:
-
{stringData.distinct.count} ({stringData.distinct.percent}%)
-
-
-
Min Length:
-
{stringData.char_stats.min_length}
-
-
-
Max Length:
-
{stringData.char_stats.max_length}
-
-
-
Median Length:
-
{stringData.char_stats.median_length}
-
-
-
Mean Length:
-
{stringData.char_stats.mean_length}
-
-
-
- + import type { StringStats } from "./types"; + import WordBarChart from "$lib/components/viz/WordBarChart.svelte"; + + let { stringData } = $props<{ stringData: StringStats }>(); + let resetZoom: boolean = $state(false); + + function resetZoomClicked() { + resetZoom = !resetZoom; + } + + +
+ +
+ +
+
+
Stats
-
- -
-
-
Word Occurrence (Top 10)
-
- -
- -
- -
- +
+
Distinct:
+
{stringData.distinct.count} ({stringData.distinct.percent}%)
-
\ No newline at end of file +
+
Min Length:
+
{stringData.char_stats.min_length}
+
+
+
Max Length:
+
{stringData.char_stats.max_length}
+
+
+
Median Length:
+
{stringData.char_stats.median_length}
+
+
+
Mean Length:
+
{stringData.char_stats.mean_length}
+
+
+
+ + +
+
+
Word Occurrence (Top 10)
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/data/getDataProfile.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/data/getDataProfile.ts new file mode 100644 index 000000000..da3d1c938 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/data/getDataProfile.ts @@ -0,0 +1,12 @@ +import { createInternalApiClient } from "$lib/api/internalClient"; +import type { DataCard } from "../card_interfaces/datacard"; +import { ServerPaths } from "$lib/components/api/routes"; + +export async function getDataProfile( + fetch: typeof window.fetch, + card: DataCard +) { + const client = createInternalApiClient(fetch); + const response = await client.post(ServerPaths.DATA_PROFILE, { card }); + return response.json(); +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/data/utils.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/data/utils.ts index 4f2e4a3de..e2127619b 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/data/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/data/utils.ts @@ -1,9 +1,6 @@ -import { getRawFile } from "$lib/components/files/utils"; -import type { DataCard } from "../card_interfaces/datacard"; import type { DataProfile, WordStats } from "$lib/components/card/data/types"; -import { RegistryType, getRegistryTableName } from "$lib/utils"; -function loadDataProfile(jsonString: string): DataProfile { +export function loadDataProfile(jsonString: string): DataProfile { try { // Parse the JSON string const parsedData = JSON.parse(jsonString); @@ -15,19 +12,6 @@ function loadDataProfile(jsonString: string): DataProfile { } } -export async function getDataProfile(card: DataCard): Promise { - let dataProfileUri = card.metadata.interface_metadata.save_metadata - .data_profile_uri as string; - - let profilePath = `${getRegistryTableName(RegistryType.Data)}/${card.space}/${ - card.name - }/v${card.version}/${dataProfileUri}`; - - let rawFile = await getRawFile(profilePath, card.uid, RegistryType.Data); - - return loadDataProfile(rawFile.content); -} - export function getSortedFeatureNames(dataProfile: DataProfile): string[] { return Object.keys(dataProfile.features).sort(); } diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/ExperimentPage.svelte similarity index 93% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.svelte rename to crates/opsml_server/opsml_ui/src/lib/components/card/experiment/ExperimentPage.svelte index d544a739a..cc3929e84 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/ExperimentPage.svelte @@ -1,23 +1,18 @@ -
-
-
{#if data.readme.exists}
@@ -40,14 +35,10 @@
{/if}
-
-
- - - \ No newline at end of file +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/VizBody.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/VizBody.svelte index d478b6bab..31fe8155e 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/VizBody.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/experiment/VizBody.svelte @@ -1,13 +1,9 @@ + + +
+
+ +

+ + +
/
+
{metadata.version}
+

+ + +
+
+ +
+ {@render children()} +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ExperimentCardLayout.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ExperimentCardLayout.svelte new file mode 100644 index 000000000..7b15b291c --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ExperimentCardLayout.svelte @@ -0,0 +1,143 @@ + + +
+
+

+ + + +
{metadata.version}
+

+ + +
+
+ +
+ {@render children()} +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ModelCardLayout.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ModelCardLayout.svelte new file mode 100644 index 000000000..64473b4ea --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ModelCardLayout.svelte @@ -0,0 +1,125 @@ + + +
+
+

+ + +
/
+
{metadata.version}
+

+ + +
+
+ +
+ {@render children()} +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/PromptCardLayout.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/PromptCardLayout.svelte new file mode 100644 index 000000000..b869fd771 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/PromptCardLayout.svelte @@ -0,0 +1,128 @@ + + + +
+
+ +

+ + + +
{metadata.version}
+

+ + + +
+
+ +
+ {@render children()} +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ServiceCardLayout.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ServiceCardLayout.svelte new file mode 100644 index 000000000..bbc9d72c8 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/layouts/ServiceCardLayout.svelte @@ -0,0 +1,123 @@ + + + +
+
+ +

+ + + +
{metadata.version}
+

+ + + +
+
+ +
+ {@render children()} +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/model/ModelPage.svelte similarity index 86% rename from crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.svelte rename to crates/opsml_server/opsml_ui/src/lib/components/card/model/ModelPage.svelte index 02338397c..21902ec85 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/model/ModelPage.svelte @@ -1,19 +1,16 @@ + let { data } = $props(); + let card: ModelCard = $state(data.metadata); +
-
-
{#if data.readme.exists}
@@ -36,7 +33,6 @@
{/if}
-
import { DriftType } from "./types"; - import { type DriftConfigType, type DriftProfile, type DriftProfileResponse, type UiProfile } from "./util"; + import { type DriftConfigType, type UiProfile } from "./utils"; import { Clock } from 'lucide-svelte'; import { TimeInterval } from '$lib/components/card/monitoring/types'; - import Dropdown from '$lib/components/utils/Dropdown.svelte'; import { KeySquare } from 'lucide-svelte'; import CustomConfigHeader from "./custom/CustomConfigHeader.svelte"; import PsiConfigHeader from "./psi/PsiConfigHeader.svelte"; import SpcConfigHeader from "./spc/SpcConfigHeader.svelte"; import LLMConfigHeader from "./llm/LLMConfigHeader.svelte"; import type { RegistryType } from "$lib/utils"; + import ComboBoxDropDown from "$lib/components/utils/ComboBoxDropDown.svelte"; // props @@ -45,7 +45,6 @@ let previousName = $state(currentName); let previousTimeInterval = $state(currentTimeInterval); - // Effect for handling a name change from the dropdown $effect(() => { if (currentName && currentName !== previousName) { @@ -66,9 +65,8 @@
- -
-
+
+
Drift Type:
{#each availableDriftTypes as drift_type} {#if drift_type === currentDriftType} @@ -84,31 +82,37 @@ {/each}
-
-
-
- +
+ +
+
Time Interval:
+
+
+ +
+ +
- -
- -
-
- + + +
+
Name:
+
+
+ +
+ +
- -
@@ -151,7 +155,4 @@ /> {/if}
-
- - - +
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/MonitoringDashboard.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/MonitoringDashboard.svelte new file mode 100644 index 000000000..6b6709bfb --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/MonitoringDashboard.svelte @@ -0,0 +1,263 @@ + + +
+
+ + +
+
+
+ + +
+ {#if currentName && latestMetrics} + {#if currentMetricData} + + {:else} +
+ No data available for selected metric +
+ {/if} + {:else} +
+ Select a metric to view data +
+ {/if} +
+ + +
+ +
+ + + {#if currentLLMRecordPage} +
+ +
+ {/if} +
+
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/VizBody.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/VizBody.svelte index f534f6d33..540a8ec33 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/VizBody.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/VizBody.svelte @@ -7,12 +7,12 @@ import type { MetricData, SpcDriftFeature, BinnedPsiMetric, BinnedMetric, BinnedMetricStats } from '$lib/components/card/monitoring/types'; import Pill from '$lib/components/utils/Pill.svelte'; import { TimeInterval } from '$lib/components/card/monitoring/types'; - import { type DriftConfigType } from './util'; + import { type DriftConfigType } from './utils'; import CustomAlertPill from '$lib/components/card/monitoring/custom/CustomAlertPill.svelte'; import LLMAlertPill from '$lib/components/card/monitoring/llm/LLMAlertPill.svelte'; import { getCustomAlertCondition, type CustomMetricDriftConfig } from './custom/custom'; import { getLLMAlertCondition, type LLMDriftConfig } from './llm/llm'; - import { type DriftProfile } from './util'; + import { type DriftProfile } from './utils'; let { metricData, diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/AlertTable.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/AlertTable.svelte index 677535f3a..f1836a840 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/AlertTable.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/AlertTable.svelte @@ -2,8 +2,6 @@ import type { Alert } from "./types"; import CodeModal from "../CodeModal.svelte"; - - let { alerts, // need to make an effect that updates this when the alerts change updateAlert @@ -17,7 +15,7 @@ let {
-
Drift Alerts
+
Drift Alerts
{#if alerts.length === 0}
No alerts to display @@ -25,34 +23,34 @@ let { {:else}
- + - - - - - - {#each alerts as alert, i} - - + - - + + - + {/each} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/utils.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/utils.ts index cc5faf145..90b4e7d99 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/alert/utils.ts @@ -1,71 +1,20 @@ -import { RoutePaths } from "$lib/components/api/routes"; -import type { - AlertResponse, - DriftAlertRequest, - Alert, - UpdateAlertResponse, - UpdateAlertStatus, -} from "./types"; -import { opsmlClient } from "$lib/components/api/client.svelte"; -import type { TimeInterval } from "../types"; -import { timeIntervalToDateTime } from "../util"; -import { sampleAlerts } from "../example"; -import { userStore } from "$lib/components/user/user.svelte"; +import { createInternalApiClient } from "$lib/api/internalClient"; +import { ServerPaths } from "$lib/components/api/routes"; -export async function getDriftAlerts( - space: string, - name: string, - version: string, - timeInterval: TimeInterval, - active: boolean -): Promise { - // For testing purposes, return sample alerts - let alertRequest: DriftAlertRequest = { - space: space, - name: name, - version: version, - limit_datetime: timeIntervalToDateTime(timeInterval), - active: active, - }; - - const response = await opsmlClient.get( - RoutePaths.DRIFT_ALERT, - alertRequest, - userStore.jwt_token - ); - if (!response.ok) { - throw new Error(`Failed to fetch drift alerts: ${response.status}`); - } - const alertResponse = (await response.json()) as AlertResponse; - - return alertResponse.alerts; -} - -//// Acknowledge an alert by its ID -export async function acknowledgeAlert( +export async function acknowledgeMonitoringAlert( + fetch: typeof globalThis.fetch, id: number, space: string ): Promise { - const request: UpdateAlertStatus = { - id: id, - active: false, - space: space, - }; - - const response = await opsmlClient.put( - RoutePaths.DRIFT_ALERT, - request, - userStore.jwt_token + let response = await createInternalApiClient(fetch).put( + ServerPaths.ACKNOWLEDGE_ALERT, + { + id: id, + active: false, + space: space, + } ); - if (!response.ok) { - throw new Error(`Failed to acknowledge alert: ${response.status}`); - } - const updateResponse = (await response.json()) as UpdateAlertResponse; - - if (!updateResponse.updated) { - throw new Error("Failed to acknowledge alert"); - } - - return updateResponse.updated; + let booleanResponse = (await response.json()) as boolean; + return booleanResponse; } diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/custom/mocks.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/custom/mocks.ts new file mode 100644 index 000000000..395762b3a --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/custom/mocks.ts @@ -0,0 +1,136 @@ +import { DriftType } from "../types"; +import { AlertThreshold, type CustomDriftProfile } from "./custom"; +import type { BinnedMetrics } from "../types"; +/** + * Mock CustomDriftProfile for UI development and testing. + * Includes realistic metric values, config, and alert conditions. + */ +export const mockCustomDriftProfile: CustomDriftProfile = { + metrics: { + custom: 0.94, + f1_score: 0.88, + latency_ms: 250, + }, + config: { + sample_size: 100, + sample: true, + space: "services", + name: "api_monitor", + version: "2.1.0", + drift_type: DriftType.Custom, + alert_config: { + dispatch_config: { + Console: { enabled: true }, + Slack: { channel: "#custom-alerts" }, + }, + schedule: "0 */4 * * *", // Every 4 hours + alert_conditions: { + custom: { + alert_threshold: AlertThreshold.Below, + alert_threshold_value: 0.9, + }, + f1_score: { + alert_threshold: AlertThreshold.Above, + alert_threshold_value: 0.85, + }, + latency_ms: { + alert_threshold: AlertThreshold.Below, + alert_threshold_value: 1000, + }, + }, + }, + }, + scouter_version: "1.0.0", +}; + +export const mockCustomMetrics: BinnedMetrics = { + metrics: { + custom: { + metric: "custom", + created_at: [ + "2025-03-25 00:43:59", + "2025-03-26 10:00:00", + "2025-03-27 11:00:00", + "2025-03-28 12:00:00", + "2025-03-29 12:00:00", + ], + stats: [ + { + avg: 0.95, + lower_bound: 0.92, + upper_bound: 0.98, + }, + { + avg: 0.94, + lower_bound: 0.91, + upper_bound: 0.97, + }, + { + avg: 0.96, + lower_bound: 0.93, + upper_bound: 0.99, + }, + { + avg: 0.9, + lower_bound: 0.93, + upper_bound: 0.99, + }, + { + avg: 0.4, + lower_bound: 0.93, + upper_bound: 0.99, + }, + ], + }, + f1_score: { + metric: "f1_score", + created_at: [ + "2024-03-26T10:00:00", + "2024-03-26T11:00:00", + "2024-03-26T12:00:00", + ], + stats: [ + { + avg: 0.88, + lower_bound: 0.85, + upper_bound: 0.91, + }, + { + avg: 0.87, + lower_bound: 0.84, + upper_bound: 0.9, + }, + { + avg: 0.89, + lower_bound: 0.86, + upper_bound: 0.92, + }, + ], + }, + latency_ms: { + metric: "latency_ms", + created_at: [ + "2024-03-26T10:00:00", + "2024-03-26T11:00:00", + "2024-03-26T12:00:00", + ], + stats: [ + { + avg: 150.5, + lower_bound: 145.0, + upper_bound: 156.0, + }, + { + avg: 148.2, + lower_bound: 143.5, + upper_bound: 153.0, + }, + { + avg: 152.8, + lower_bound: 147.2, + upper_bound: 158.4, + }, + ], + }, + }, +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/example.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/example.ts deleted file mode 100644 index 019b13770..000000000 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/example.ts +++ /dev/null @@ -1,416 +0,0 @@ -import type { - BinnedMetrics, - BinnedPsiFeatureMetrics, - BinnedSpcFeatureMetrics, - LLMDriftServerRecord, - LLMPageResponse, - PaginationCursor, -} from "./types"; -import { Status } from "./types"; -import type { Alert } from "./alert/types"; - -const sampleCustomMetrics: BinnedMetrics = { - metrics: { - custom: { - metric: "custom", - created_at: [ - "2025-03-25 00:43:59", - "2025-03-26 10:00:00", - "2025-03-27 11:00:00", - "2025-03-28 12:00:00", - "2025-03-29 12:00:00", - ], - stats: [ - { - avg: 0.95, - lower_bound: 0.92, - upper_bound: 0.98, - }, - { - avg: 0.94, - lower_bound: 0.91, - upper_bound: 0.97, - }, - { - avg: 0.96, - lower_bound: 0.93, - upper_bound: 0.99, - }, - { - avg: 0.9, - lower_bound: 0.93, - upper_bound: 0.99, - }, - { - avg: 0.4, - lower_bound: 0.93, - upper_bound: 0.99, - }, - ], - }, - f1_score: { - metric: "f1_score", - created_at: [ - "2024-03-26T10:00:00", - "2024-03-26T11:00:00", - "2024-03-26T12:00:00", - ], - stats: [ - { - avg: 0.88, - lower_bound: 0.85, - upper_bound: 0.91, - }, - { - avg: 0.87, - lower_bound: 0.84, - upper_bound: 0.9, - }, - { - avg: 0.89, - lower_bound: 0.86, - upper_bound: 0.92, - }, - ], - }, - latency_ms: { - metric: "latency_ms", - created_at: [ - "2024-03-26T10:00:00", - "2024-03-26T11:00:00", - "2024-03-26T12:00:00", - ], - stats: [ - { - avg: 150.5, - lower_bound: 145.0, - upper_bound: 156.0, - }, - { - avg: 148.2, - lower_bound: 143.5, - upper_bound: 153.0, - }, - { - avg: 152.8, - lower_bound: 147.2, - upper_bound: 158.4, - }, - ], - }, - }, -}; - -const sampleLLMMetrics: BinnedMetrics = { - metrics: { - reformulation_quality: { - metric: "reformulation_quality", - created_at: [ - "2025-03-25 00:43:59", - "2025-03-26 10:00:00", - "2025-03-27 11:00:00", - "2025-03-28 12:00:00", - "2025-03-29 12:00:00", - ], - stats: [ - { - avg: 0.95, - lower_bound: 0.92, - upper_bound: 0.98, - }, - { - avg: 0.94, - lower_bound: 0.91, - upper_bound: 0.97, - }, - { - avg: 0.96, - lower_bound: 0.93, - upper_bound: 0.99, - }, - { - avg: 0.9, - lower_bound: 0.93, - upper_bound: 0.99, - }, - { - avg: 0.4, - lower_bound: 0.93, - upper_bound: 0.99, - }, - ], - }, - coherence: { - metric: "coherence", - created_at: [ - "2024-03-26T10:00:00", - "2024-03-26T11:00:00", - "2024-03-26T12:00:00", - ], - stats: [ - { - avg: 0.88, - lower_bound: 0.85, - upper_bound: 0.91, - }, - { - avg: 0.87, - lower_bound: 0.84, - upper_bound: 0.9, - }, - { - avg: 0.89, - lower_bound: 0.86, - upper_bound: 0.92, - }, - ], - }, - }, -}; - -const sampleSpcMetrics: BinnedSpcFeatureMetrics = { - features: { - col_0: { - created_at: [ - "2025-03-25 00:43:59", - "2025-03-26 10:00:00", - "2025-03-27 11:00:00", - "2025-03-28 12:00:00", - "2025-03-29 12:00:00", - ], - values: [100, 105, 200, 1025, 101], - }, - col_1: { - created_at: [ - "2025-03-25 00:43:59", - "2025-03-26 10:00:00", - "2025-03-27 11:00:00", - "2025-03-28 12:00:00", - "2025-03-29 12:00:00", - ], - values: [100, 105, 200, 1025, 101], - }, - }, -}; - -const samplePsiMetrics: BinnedPsiFeatureMetrics = { - features: { - col_0: { - created_at: [ - "2025-03-25 00:43:59", - "2025-03-26 10:00:00", - "2025-03-27 11:00:00", - "2025-03-28 12:00:00", - "2025-03-29 12:00:00", - ], - psi: [0.05, 0.07, 0.04, 0.1, 0.05], - overall_psi: 0.053, - bins: { - 0: 0.1, - 1: 0.2, - 2: 0.3, - 3: 0.25, - 4: 0.15, - }, - }, - col_1: { - created_at: [ - "2025-03-25 00:43:59", - "2025-03-25 01:43:59", - "2025-03-25 02:43:59", - "2025-03-25 03:43:59", - "2025-03-25 04:43:59", - "2025-03-25 05:43:59", - "2025-03-25 06:43:59", - "2025-03-25 07:43:59", - "2025-03-25 08:43:59", - "2025-03-25 09:43:59", - ], - psi: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5], - overall_psi: 0.053, - bins: { - 0: 0.1, - 1: 0.2, - 2: 0.3, - 3: 0.25, - 4: 0.15, - }, - }, - }, -}; - -export { - sampleSpcMetrics, - samplePsiMetrics, - sampleCustomMetrics, - sampleLLMMetrics, -}; - -export const sampleAlerts: Alert[] = [ - { - created_at: "2024-03-28 10:30:00", - name: "credit_model", - space: "models", - version: "1.0.0", - entity_name: "credit_score", - alert: { type: "drift_detected", message: "PSI value exceeded threshold" }, - id: 1, - drift_type: "psi", - active: true, - }, - { - created_at: "2024-03-28 09:45:00", - name: "fraud_detection", - space: "models", - version: "2.1.0", - entity_name: "transaction_amount", - alert: { type: "spc_violation", message: "Value outside control limits" }, - id: 2, - drift_type: "spc", - active: true, - }, - { - created_at: "2024-03-28 09:00:00", - name: "customer_churn", - space: "ml_models", - version: "1.2.3", - entity_name: "usage_frequency", - alert: { type: "custom_metric", message: "Metric below threshold" }, - id: 3, - drift_type: "custom", - active: true, - }, - { - created_at: "2024-03-27 23:15:00", - name: "recommendation_engine", - space: "recsys", - version: "3.0.1", - entity_name: "user_engagement", - alert: { type: "drift_detected", message: "Distribution shift detected" }, - id: 4, - drift_type: "psi", - active: false, - }, - { - created_at: "2024-03-27 22:30:00", - name: "credit_model", - space: "models", - version: "1.0.0", - entity_name: "debt_ratio", - alert: { type: "spc_violation", message: "Consecutive points above mean" }, - id: 5, - drift_type: "spc", - active: true, - }, - { - created_at: "2024-03-27 21:45:00", - name: "fraud_detection", - space: "models", - version: "2.1.0", - entity_name: "ip_velocity", - alert: { type: "psi_threshold", message: "PSI above 0.2" }, - id: 6, - drift_type: "psi", - active: true, - }, - { - created_at: "2024-03-27 20:00:00", - name: "price_optimization", - space: "pricing", - version: "1.1.0", - entity_name: "demand_forecast", - alert: { type: "custom_metric", message: "Accuracy below target" }, - id: 7, - drift_type: "custom", - active: false, - }, - { - created_at: "2024-03-27 19:15:00", - name: "customer_churn", - space: "ml_models", - version: "1.2.3", - entity_name: "support_tickets", - alert: { type: "drift_detected", message: "Significant feature drift" }, - id: 8, - drift_type: "psi", - active: true, - }, - { - created_at: "2024-03-27 18:30:00", - name: "recommendation_engine", - space: "recsys", - version: "3.0.1", - entity_name: "click_through_rate", - alert: { type: "spc_violation", message: "Point beyond 3 sigma" }, - id: 9, - drift_type: "spc", - active: true, - }, - { - created_at: "2024-03-27 17:45:00", - name: "price_optimization", - space: "pricing", - version: "1.1.0", - entity_name: "competitor_prices", - alert: { type: "custom_metric", message: "Data freshness warning" }, - id: 10, - drift_type: "custom", - active: true, - }, -]; - -function randomStatus(): Status { - const statuses = [ - Status.Pending, - Status.Processing, - Status.Processed, - Status.Failed, - ]; - return statuses[Math.floor(Math.random() * statuses.length)]; -} - -function randomDate(offsetDays: number = 0): string { - const date = new Date(); - date.setDate(date.getDate() - offsetDays); - return date.toISOString(); -} - -export const mockLLMDriftServerRecords: LLMDriftServerRecord[] = Array.from( - { length: 30 }, - (_, i) => ({ - created_at: randomDate(30 - i), - space: `space_${(i % 5) + 1}`, - name: `card_${(i % 10) + 1}`, - version: `v${(i % 3) + 1}.0.${i}`, - prompt: i % 2 === 0 ? `Prompt for card_${(i % 10) + 1}` : undefined, - context: `Context for card_${(i % 10) + 1}`, - status: randomStatus(), - id: i + 1, - uid: `uid_${i + 1}`, - score: { - relevance: { - score: Number(Math.random().toFixed(2)), - reason: - "The prompt is relevant and you should use it!. AHHHHHHHHHHHHHHHHHHHHHHHHH", - }, - coherence: { - score: Number(Math.random().toFixed(2)), - reason: "Sample reason", - }, - }, - updated_at: randomDate(29 - i), - processing_started_at: i % 3 === 0 ? randomDate(30 - i) : undefined, - processing_ended_at: i % 4 === 0 ? randomDate(29 - i) : undefined, - processing_duration: - i % 5 === 0 ? Math.floor(Math.random() * 60) : undefined, - }) -); - -let paginationCursor: PaginationCursor = { - id: mockLLMDriftServerRecords.length, -}; -export const mockLLMDriftPageResponse: LLMPageResponse = { - items: mockLLMDriftServerRecords, - next_cursor: paginationCursor, - has_more: true, -}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/getMonitoringDashboardData.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/getMonitoringDashboardData.ts new file mode 100644 index 000000000..c602e2784 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/getMonitoringDashboardData.ts @@ -0,0 +1,236 @@ +import { getMaxDataPoints, RegistryType } from "$lib/utils"; +import { + getLatestMonitoringMetrics, + getLLMMonitoringRecordPage, + getMonitoringAlerts, + getMonitoringDriftProfiles, + getProfileConfig, + getProfileFeatures, + getCurrentMetricData, + type UiProfile, + type DriftProfileResponse, +} from "$lib/components/card/monitoring/utils"; +import { + DriftType, + TimeInterval, + type ServiceInfo, + type BinnedDriftMap, + type MetricData, + type DriftProfileUri, +} from "$lib/components/card/monitoring/types"; +import type { Alert } from "$lib/components/card/monitoring/alert/types"; + +/** + * Parent data interface expected from SvelteKit page hierarchy + * Supports different metadata structures for different registry types + */ +export interface MonitoringParentData { + metadata: { + uid: string; + metadata: { + // For Prompt registries - direct drift profile URI map + drift_profile_uri_map?: Record; + // For Model/Data/Experiment registries - nested structure + interface_metadata?: { + save_metadata?: { + drift_profile_uri_map?: Record; + }; + }; + }; + }; + registryType: RegistryType; +} + +/** + * Extracts the drift profile URI map from metadata based on registry type + * + * Different registry types store drift profile URI maps in different locations: + * - Prompt registries: metadata.metadata.drift_profile_uri_map + * - Model/Data/Experiment registries: metadata.metadata.interface_metadata.save_metadata.drift_profile_uri_map + * + * @param metadata - The metadata object containing drift profile information + * @param registryType - The type of registry (Prompt, Model, Data, or Experiment) + * @returns Record of drift profile URI mappings, empty object if none found + */ +function getDriftProfileUriMap( + metadata: MonitoringParentData["metadata"], + registryType: RegistryType +): Record { + if (registryType === RegistryType.Prompt) { + return metadata.metadata.drift_profile_uri_map ?? {}; + } + + // Model, Data, Experiment registries + return ( + metadata.metadata.interface_metadata?.save_metadata + ?.drift_profile_uri_map ?? {} + ); +} + +/** + * Configuration options for monitoring dashboard data loading + */ +export interface MonitoringDashboardLoadOptions { + /** Initial time interval for metrics, defaults to 6 hours */ + initialTimeInterval?: TimeInterval; + /** Whether to load LLM records for prompt registries, defaults to true */ + loadLLMRecords?: boolean; + /** Whether to load alerts, defaults to true */ + loadAlerts?: boolean; +} + +/** + * Return type for monitoring dashboard data + */ +export interface MonitoringDashboardData { + profiles: DriftProfileResponse; + keys: DriftType[]; + currentName: string; + currentNames: string[]; + currentDriftType: DriftType; + currentProfile: UiProfile; + currentConfig: ReturnType; + latestMetrics: BinnedDriftMap; + currentMetricData: MetricData; + maxDataPoints: number; + currentAlerts: Alert[]; + currentLLMRecords?: any; // Type this more specifically based on your LLM record structure +} + +/** + * Loads monitoring dashboard data for SvelteKit pages + * + * This function encapsulates the common data loading logic for monitoring + * dashboards, providing consistent initialization across different routes. + * + * @param fetch - SvelteKit fetch function for server-side requests + * @param parentData - Data from parent layout loaders + * @param options - Configuration options for data loading + * @returns Promise resolving to monitoring dashboard data + * + * @example + * ```typescript + * // In your +page.ts file + * export const load: PageLoad = async ({ parent, fetch }) => { + * const parentData = await parent(); + * return await loadMonitoringDashboardData(fetch, parentData); + * }; + * ``` + */ +export async function loadMonitoringDashboardData( + fetch: typeof globalThis.fetch, + parentData: MonitoringParentData, + options: MonitoringDashboardLoadOptions = {} +): Promise { + const { + initialTimeInterval = TimeInterval.SixHours, + loadLLMRecords = true, + loadAlerts = true, + } = options; + + const { metadata, registryType } = parentData; + + // Extract drift profile URI map based on registry type + const profileMap = getDriftProfileUriMap(metadata, registryType); + + // Load drift profiles + const profiles = await getMonitoringDriftProfiles( + fetch, + metadata.uid, + profileMap, + registryType + ); + + // Extract and sort available drift types + const keys: DriftType[] = Object.keys(profiles) + .filter((key): key is DriftType => + Object.values(DriftType).includes(key as DriftType) + ) + .sort(); + + // Validate that we have at least one drift type + if (keys.length === 0) { + throw new Error("No valid drift types found in profiles"); + } + + // Initialize current selections with first available drift type + const currentDriftType = keys[0]; + const currentProfile: UiProfile = profiles[currentDriftType]; + const currentNames: string[] = getProfileFeatures( + currentDriftType, + currentProfile.profile + ); + + // Validate that we have at least one feature name + if (currentNames.length === 0) { + throw new Error( + `No feature names found for drift type: ${currentDriftType}` + ); + } + + const currentName: string = currentNames[0]; + const currentConfig = getProfileConfig( + currentDriftType, + currentProfile.profile + ); + const maxDataPoints = getMaxDataPoints(); + + // Load metrics data + const latestMetrics = await getLatestMonitoringMetrics( + fetch, + profiles, + initialTimeInterval, + maxDataPoints + ); + + // Get current metric data for selected drift type and name + const currentMetricData = getCurrentMetricData( + latestMetrics, + currentDriftType, + currentName + ); + + // Load alerts if enabled + const currentAlerts = loadAlerts + ? await getMonitoringAlerts( + fetch, + currentConfig.space, + currentConfig.name, + currentConfig.version, + initialTimeInterval, + true + ) + : []; + + // Load LLM records for prompt registries if enabled + let currentLLMRecords; + if (loadLLMRecords && registryType === RegistryType.Prompt) { + const serviceInfo: ServiceInfo = { + space: currentConfig.space, + name: currentConfig.name, + version: currentConfig.version, + }; + + currentLLMRecords = await getLLMMonitoringRecordPage( + fetch, + serviceInfo, + undefined, + undefined + ); + } + + return { + profiles, + keys, + currentName, + currentNames, + currentDriftType, + currentProfile, + currentConfig, + latestMetrics, + currentMetricData, + maxDataPoints, + currentAlerts, + currentLLMRecords, + }; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/LLMRecordTable.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/LLMRecordTable.svelte index 9241e9689..be70fd73e 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/LLMRecordTable.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/LLMRecordTable.svelte @@ -1,7 +1,7 @@
-
LLM Records
+
LLM Records
{#if pageItems.length === 0}
No LLM Drift Records Found
{:else}
-
+ ID + Drift Type + Name + Details + Status + Created At @@ -62,9 +60,9 @@ let {
{alert.id} - {alert.id} + {alert.entity_name}{alert.entity_name}
{alert.created_at}{alert.created_at}
- +
+ - - - - - - - {#each pageItems as record, i} - - - + - + {#if record.prompt} - + {:else} - + {/if} {#if record.context} - + {:else} - + {/if} - - +
+ ID + Status + Score + Prompt + Context + Created At + Processing Duration @@ -116,10 +115,10 @@ let {
{record.id} - + {record.id} + {record.created_at} + {record.created_at} {record.processing_duration !== undefined ? `${record.processing_duration / 1000} seconds` : 'N/A'} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/mocks.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/mocks.ts new file mode 100644 index 000000000..a837c0909 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/llm/mocks.ts @@ -0,0 +1,286 @@ +import { + DriftType, + Status, + type LLMDriftServerRecord, + type LLMPageResponse, + type PaginationCursor, +} from "../types"; +import { AlertThreshold } from "./llm"; +import { type BinnedMetrics } from "../types"; +import { + type Prompt, + Provider, + Role, + ResponseType, +} from "$lib/components/genai/types"; +import type { LLMDriftProfile } from "./llm"; +import type { OpenAIChatSettings } from "$lib/components/genai/settings/openai"; +import type { GeminiSettings } from "$lib/components/genai/settings/gemini"; + +/** + * Mock OpenAI Prompt + */ +export const mockOpenAIPrompt: Prompt = { + message: [ + { + content: { Str: "What is the capital of France?" }, + role: Role.User, + variables: [], + }, + { + content: { Str: "The capital of France is Paris." }, + role: Role.Assistant, + variables: [], + }, + ], + system_instruction: [ + { + content: { Str: "You are a helpful assistant." }, + role: Role.Assistant || "system", + variables: [], + }, + ], + model_settings: { + OpenAIChat: { + max_completion_tokens: 128, + temperature: 0.7, + top_p: 1, + stop_sequences: ["\n"], + audio: { format: "mp3", voice: "en-US-Wavenet-D" }, + tool_choice: { mode: "auto" }, + tools: [], + metadata: { project: "demo" }, + } as OpenAIChatSettings, + }, + model: "gpt-4", + provider: Provider.OpenAI, + version: "1.0.0", + response_json_schema: undefined, + parameters: ["capital", "country"], + response_type: ResponseType.Pydantic, +}; + +/** + * Mock Gemini Prompt + */ +export const mockGeminiPrompt: Prompt = { + message: [ + { + content: { Str: "Summarize the attached document." }, + role: Role.User, + variables: [], + }, + { + content: { Str: "Here is the summary of your document..." }, + role: Role.Assistant, + variables: [], + }, + ], + system_instruction: [ + { + content: { Str: "You are an expert summarizer." }, + role: Role.Assistant || "system", + variables: [], + }, + ], + model_settings: { + GoogleChat: { + labels: { project: "demo-gemini" }, + generation_config: { + temperature: 0.5, + top_p: 0.9, + max_output_tokens: 256, + stop_sequences: [""], + response_modalities: ["Text"], + }, + safety_settings: [ + { + category: "HarmCategoryUnspecified", + threshold: "BlockNone", + }, + ], + } as GeminiSettings, + }, + model: "gemini-pro", + provider: Provider.Gemini, + version: "1.0.0", + response_json_schema: undefined, + parameters: ["summary", "document"], + response_type: ResponseType.Score, +}; + +/** + * Mock LLMDriftProfile for UI development and testing. + * Includes realistic metrics, config, and alert conditions for LLM drift monitoring. + */ +export const mockLLMDriftProfile: LLMDriftProfile = { + metrics: [ + { + name: "toxicity_score", + value: 0.12, + prompt: mockOpenAIPrompt, + alert_condition: { + alert_threshold: AlertThreshold.Above, + alert_threshold_value: 0.1, + }, + }, + { + name: "coherence_score", + value: 0.85, + prompt: mockGeminiPrompt, + alert_condition: { + alert_threshold: AlertThreshold.Below, + alert_threshold_value: 0.8, + }, + }, + ], + metric_names: ["toxicity_score", "coherence_score"], + config: { + sample_rate: 50, + space: "llm-services", + name: "summarizer", + version: "3.0.0", + drift_type: DriftType.LLM, + alert_config: { + dispatch_config: { + Console: { enabled: true }, + Slack: { channel: "#llm-alerts" }, + }, + schedule: "0 */2 * * *", // Every 2 hours + alert_conditions: { + toxicity_score: { + alert_threshold: AlertThreshold.Above, + alert_threshold_value: 0.1, + }, + coherence_score: { + alert_threshold: AlertThreshold.Below, + alert_threshold_value: 0.8, + }, + }, + }, + }, + scouter_version: "2.0.0", +}; + +export const mockLLMMetrics: BinnedMetrics = { + metrics: { + toxicity_score: { + metric: "toxicity_score", + created_at: [ + "2025-03-25 00:43:59", + "2025-03-26 10:00:00", + "2025-03-27 11:00:00", + "2025-03-28 12:00:00", + "2025-03-29 12:00:00", + ], + stats: [ + { + avg: 0.95, + lower_bound: 0.92, + upper_bound: 0.98, + }, + { + avg: 0.94, + lower_bound: 0.91, + upper_bound: 0.97, + }, + { + avg: 0.96, + lower_bound: 0.93, + upper_bound: 0.99, + }, + { + avg: 0.9, + lower_bound: 0.93, + upper_bound: 0.99, + }, + { + avg: 0.4, + lower_bound: 0.93, + upper_bound: 0.99, + }, + ], + }, + coherence_score: { + metric: "coherence_score", + created_at: [ + "2024-03-26T10:00:00", + "2024-03-26T11:00:00", + "2024-03-26T12:00:00", + ], + stats: [ + { + avg: 0.88, + lower_bound: 0.85, + upper_bound: 0.91, + }, + { + avg: 0.87, + lower_bound: 0.84, + upper_bound: 0.9, + }, + { + avg: 0.89, + lower_bound: 0.86, + upper_bound: 0.92, + }, + ], + }, + }, +}; + +function randomStatus(): Status { + const statuses = [ + Status.Pending, + Status.Processing, + Status.Processed, + Status.Failed, + ]; + return statuses[Math.floor(Math.random() * statuses.length)]; +} + +function randomDate(offsetDays: number = 0): string { + const date = new Date(); + date.setDate(date.getDate() - offsetDays); + return date.toISOString(); +} + +export const mockLLMDriftServerRecords: LLMDriftServerRecord[] = Array.from( + { length: 30 }, + (_, i) => ({ + created_at: randomDate(30 - i), + space: `space_${(i % 5) + 1}`, + name: `card_${(i % 10) + 1}`, + version: `v${(i % 3) + 1}.0.${i}`, + prompt: i % 2 === 0 ? `Prompt for card_${(i % 10) + 1}` : undefined, + context: `Context for card_${(i % 10) + 1}`, + status: randomStatus(), + id: i + 1, + uid: `uid_${i + 1}`, + score: { + relevance: { + score: Number(Math.random().toFixed(2)), + reason: + "The prompt is relevant and you should use it!. AHHHHHHHHHHHHHHHHHHHHHHHHH", + }, + coherence: { + score: Number(Math.random().toFixed(2)), + reason: "Sample reason", + }, + }, + updated_at: randomDate(29 - i), + processing_started_at: i % 3 === 0 ? randomDate(30 - i) : undefined, + processing_ended_at: i % 4 === 0 ? randomDate(29 - i) : undefined, + processing_duration: + i % 5 === 0 ? Math.floor(Math.random() * 60) : undefined, + }) +); + +let paginationCursor: PaginationCursor = { + id: mockLLMDriftServerRecords.length, +}; +export const mockLLMDriftPageResponse: LLMPageResponse = { + items: mockLLMDriftServerRecords, + next_cursor: paginationCursor, + has_more: true, +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/mocks.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/mocks.ts new file mode 100644 index 000000000..510a5974b --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/mocks.ts @@ -0,0 +1,163 @@ +import { DriftType } from "./types"; +import { mockPsiDriftProfile } from "./psi/mocks"; +import { mockCustomDriftProfile } from "./custom/mocks"; +import { mockSpcDriftProfile } from "./spc/mocks"; +import { mockLLMDriftProfile } from "./llm/mocks"; +import type { UiProfile } from "./utils"; +import type { Alert } from "./alert/types"; + +/** + * Mock DriftProfileResponse for testing and UI development. + * Each profile uses a realistic mock and a sample URI. + */ +export const mockDriftProfileResponse: Record = { + [DriftType.Spc]: { + profile_uri: "/profiles/spc/mock.json", + profile: { + Spc: mockSpcDriftProfile, + Psi: mockPsiDriftProfile, + Custom: mockCustomDriftProfile, + LLM: mockLLMDriftProfile, + }, + }, + [DriftType.Psi]: { + profile_uri: "/profiles/psi/mock.json", + profile: { + Spc: mockSpcDriftProfile, + Psi: mockPsiDriftProfile, + Custom: mockCustomDriftProfile, + LLM: mockLLMDriftProfile, + }, + }, + [DriftType.Custom]: { + profile_uri: "/profiles/custom/mock.json", + profile: { + Spc: mockSpcDriftProfile, + Psi: mockPsiDriftProfile, + Custom: mockCustomDriftProfile, + LLM: mockLLMDriftProfile, + }, + }, + [DriftType.LLM]: { + profile_uri: "/profiles/llm/mock.json", + profile: { + Spc: mockSpcDriftProfile, + Psi: mockPsiDriftProfile, + Custom: mockCustomDriftProfile, + LLM: mockLLMDriftProfile, + }, + }, +}; + +export const mockAlerts: Alert[] = [ + { + created_at: "2024-03-28 10:30:00", + name: "credit_model", + space: "models", + version: "1.0.0", + entity_name: "credit_score", + alert: { type: "drift_detected", message: "PSI value exceeded threshold" }, + id: 1, + drift_type: "psi", + active: true, + }, + { + created_at: "2024-03-28 09:45:00", + name: "fraud_detection", + space: "models", + version: "2.1.0", + entity_name: "transaction_amount", + alert: { type: "spc_violation", message: "Value outside control limits" }, + id: 2, + drift_type: "spc", + active: true, + }, + { + created_at: "2024-03-28 09:00:00", + name: "customer_churn", + space: "ml_models", + version: "1.2.3", + entity_name: "usage_frequency", + alert: { type: "custom_metric", message: "Metric below threshold" }, + id: 3, + drift_type: "custom", + active: true, + }, + { + created_at: "2024-03-27 23:15:00", + name: "recommendation_engine", + space: "recsys", + version: "3.0.1", + entity_name: "user_engagement", + alert: { type: "drift_detected", message: "Distribution shift detected" }, + id: 4, + drift_type: "psi", + active: false, + }, + { + created_at: "2024-03-27 22:30:00", + name: "credit_model", + space: "models", + version: "1.0.0", + entity_name: "debt_ratio", + alert: { type: "spc_violation", message: "Consecutive points above mean" }, + id: 5, + drift_type: "spc", + active: true, + }, + { + created_at: "2024-03-27 21:45:00", + name: "fraud_detection", + space: "models", + version: "2.1.0", + entity_name: "ip_velocity", + alert: { type: "psi_threshold", message: "PSI above 0.2" }, + id: 6, + drift_type: "psi", + active: true, + }, + { + created_at: "2024-03-27 20:00:00", + name: "price_optimization", + space: "pricing", + version: "1.1.0", + entity_name: "demand_forecast", + alert: { type: "custom_metric", message: "Accuracy below target" }, + id: 7, + drift_type: "custom", + active: false, + }, + { + created_at: "2024-03-27 19:15:00", + name: "customer_churn", + space: "ml_models", + version: "1.2.3", + entity_name: "support_tickets", + alert: { type: "drift_detected", message: "Significant feature drift" }, + id: 8, + drift_type: "psi", + active: true, + }, + { + created_at: "2024-03-27 18:30:00", + name: "recommendation_engine", + space: "recsys", + version: "3.0.1", + entity_name: "click_through_rate", + alert: { type: "spc_violation", message: "Point beyond 3 sigma" }, + id: 9, + drift_type: "spc", + active: true, + }, + { + created_at: "2024-03-27 17:45:00", + name: "price_optimization", + space: "pricing", + version: "1.1.0", + entity_name: "competitor_prices", + alert: { type: "custom_metric", message: "Data freshness warning" }, + id: 10, + drift_type: "custom", + active: true, + }, +]; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/PsiConfigHeader.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/PsiConfigHeader.svelte index 505f0d074..abeae0b8d 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/PsiConfigHeader.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/PsiConfigHeader.svelte @@ -3,7 +3,7 @@ import type { PsiAlertConfig, PsiDriftConfig } from "./psi"; import Pill from "$lib/components/utils/Pill.svelte"; import UpdateModal from "../update/UpdateModal.svelte"; - import type { UiProfile } from "../util"; + import type { UiProfile } from "../utils"; import { onMount } from "svelte"; import type { RegistryType } from "$lib/utils"; @@ -46,9 +46,7 @@
-
Dispatch:
- {#if hasSlackConfig(alertConfig.dispatch_config)} {/if} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/mocks.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/mocks.ts new file mode 100644 index 000000000..9c01793c4 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/psi/mocks.ts @@ -0,0 +1,105 @@ +import { DriftType } from "../types"; +import { BinType, type PsiDriftProfile } from "./psi"; +import type { BinnedPsiFeatureMetrics } from "../types"; +/** + * Mock PsiDriftProfile for UI development and testing. + * Provides realistic bins, config, and alert settings for PSI drift monitoring. + */ +export const mockPsiDriftProfile: PsiDriftProfile = { + features: { + feature_a: { + id: "feature_a", + bins: [ + { id: 0, lower_limit: 18, upper_limit: 25, proportion: 0.15 }, + { id: 1, lower_limit: 26, upper_limit: 35, proportion: 0.35 }, + { id: 2, lower_limit: 36, upper_limit: 50, proportion: 0.3 }, + { id: 3, lower_limit: 51, upper_limit: null, proportion: 0.2 }, + ], + timestamp: "2025-10-13T08:00:00Z", + bin_type: BinType.Numeric, + }, + feature_b: { + id: "feature_b", + bins: [ + { id: 0, lower_limit: null, upper_limit: null, proportion: 0.55 }, // Male + { id: 1, lower_limit: null, upper_limit: null, proportion: 0.45 }, // Female + ], + timestamp: "2025-10-13T08:00:00Z", + bin_type: BinType.Binary, + }, + }, + config: { + space: "models", + name: "credit_score", + version: "1.0.0", + drift_type: DriftType.Psi, + feature_map: { + features: { + feature_a: { "2025-10-13T08:00:00Z": 30, "2025-10-13T09:00:00Z": 32 }, + feature_b: { + "2025-10-13T08:00:00Z": 0.5, + "2025-10-13T09:00:00Z": 0.52, + }, + }, + }, + targets: ["feature_a", "feature_b"], + alert_config: { + dispatch_config: { + Console: { enabled: true }, + Slack: { channel: "#psi-alerts" }, + }, + schedule: "0 */12 * * *", // Every 12 hours + features_to_monitor: ["feature_a", "feature_b"], + threshold: { + Normal: { alpha: 0.05 }, + }, + }, + }, + scouter_version: "1.2.0", +}; + +export const mockPsiMetrics: BinnedPsiFeatureMetrics = { + features: { + feature_a: { + created_at: [ + "2025-03-25 00:43:59", + "2025-03-26 10:00:00", + "2025-03-27 11:00:00", + "2025-03-28 12:00:00", + "2025-03-29 12:00:00", + ], + psi: [0.05, 0.07, 0.04, 0.1, 0.05], + overall_psi: 0.053, + bins: { + 0: 0.1, + 1: 0.2, + 2: 0.3, + 3: 0.25, + 4: 0.15, + }, + }, + feature_b: { + created_at: [ + "2025-03-25 00:43:59", + "2025-03-25 01:43:59", + "2025-03-25 02:43:59", + "2025-03-25 03:43:59", + "2025-03-25 04:43:59", + "2025-03-25 05:43:59", + "2025-03-25 06:43:59", + "2025-03-25 07:43:59", + "2025-03-25 08:43:59", + "2025-03-25 09:43:59", + ], + psi: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5], + overall_psi: 0.053, + bins: { + 0: 0.1, + 1: 0.2, + 2: 0.3, + 3: 0.25, + 4: 0.15, + }, + }, + }, +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/SpcConfigHeader.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/SpcConfigHeader.svelte index 8091a9c93..66ba1a4dc 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/SpcConfigHeader.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/SpcConfigHeader.svelte @@ -3,7 +3,7 @@ import type { SpcAlertConfig, SpcDriftConfig } from "./spc"; import Pill from "$lib/components/utils/Pill.svelte"; import UpdateModal from "../update/UpdateModal.svelte"; - import type { UiProfile } from "../util"; + import type { UiProfile } from "../utils"; import type { RegistryType } from "$lib/utils"; ; @@ -37,9 +37,8 @@
- -
Dispatch:
- +
Dispatch:
+ {#if hasSlackConfig(alertConfig.dispatch_config)} {/if} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/mocks.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/mocks.ts new file mode 100644 index 000000000..788f417fb --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/spc/mocks.ts @@ -0,0 +1,89 @@ +import { DriftType } from "$lib/components/card/monitoring/types"; +import type { BinnedSpcFeatureMetrics } from "../types"; +import { type SpcDriftProfile } from "./spc"; +import { AlertZone } from "./spc"; + +/** + * Mock SpcDriftProfile for UI development and testing. + * Includes realistic feature drift profiles, config, and alert settings. + */ +export const mockSpcDriftProfile: SpcDriftProfile = { + features: { + feature_a: { + id: "feature_a", + center: 50, + one_ucl: 60, + one_lcl: 40, + two_ucl: 65, + two_lcl: 35, + three_ucl: 70, + three_lcl: 30, + timestamp: "2025-10-13T08:00:00Z", + }, + feature_b: { + id: "feature_b", + center: 100, + one_ucl: 110, + one_lcl: 90, + two_ucl: 115, + two_lcl: 85, + three_ucl: 120, + three_lcl: 80, + timestamp: "2025-10-13T08:00:00Z", + }, + }, + config: { + sample_size: 30, + sample: true, + space: "models", + name: "credit_score", + version: "1.0.0", + drift_type: DriftType.Spc, + feature_map: { + features: { + feature_a: { "2025-10-13T08:00:00Z": 52, "2025-10-13T09:00:00Z": 48 }, + feature_b: { "2025-10-13T08:00:00Z": 102, "2025-10-13T09:00:00Z": 98 }, + }, + }, + targets: ["feature_a", "feature_b"], + alert_config: { + rule: { + rule: "Zone Rule", + zones_to_monitor: [AlertZone.Zone1, AlertZone.Zone2, AlertZone.Zone3], + }, + dispatch_config: { + Console: { enabled: true }, + Slack: { channel: "#alerts" }, + OpsGenie: { team: "ML", priority: "high" }, + }, + schedule: "0 */6 * * *", // Every 6 hours + features_to_monitor: ["feature_a", "feature_b"], + }, + }, + scouter_version: "1.2.0", +}; + +export const mockSpcMetrics: BinnedSpcFeatureMetrics = { + features: { + feature_a: { + created_at: [ + "2025-03-25 00:43:59", + "2025-03-26 10:00:00", + "2025-03-27 11:00:00", + "2025-03-28 12:00:00", + "2025-03-29 12:00:00", + ], + values: [52, 48, 47, 49, 51], + }, + feature_b: { + created_at: [ + "2025-03-25 00:43:59", + "2025-03-26 10:00:00", + "2025-03-27 11:00:00", + "2025-03-28 12:00:00", + "2025-03-29 12:00:00", + ], + values: [100, 105, 200, 300, 101], + }, + }, +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/types.ts b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/types.ts index 521c33c13..a74d2da61 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/types.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/types.ts @@ -1,5 +1,4 @@ import type { RegistryType } from "$lib/utils"; -import { string } from "zod"; export enum AlertDispatchType { Slack = "Slack", @@ -14,6 +13,12 @@ export enum DriftType { LLM = "LLM", } +export interface DriftProfileUri { + root_dir: string; + uri: string; + drift_type: DriftType; +} + export interface FeatureMap { features: Record>; } diff --git a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/update/UpdateModal.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/update/UpdateModal.svelte index d208918fd..d0bd6852b 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/update/UpdateModal.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/monitoring/update/UpdateModal.svelte @@ -1,12 +1,11 @@ @@ -49,6 +47,3 @@
- - - \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/lib/components/card/service/ServicePage.svelte similarity index 91% rename from crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.svelte rename to crates/opsml_server/opsml_ui/src/lib/components/card/service/ServicePage.svelte index e0a23cfbd..a3384a58d 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/card/service/ServicePage.svelte @@ -1,16 +1,15 @@ -
+
-
+
{#each splitPath as path, index} {#if index < 3} @@ -68,7 +68,7 @@
-
+
diff --git a/crates/opsml_server/opsml_ui/src/lib/components/files/FileViewer.svelte b/crates/opsml_server/opsml_ui/src/lib/components/files/FileViewer.svelte index 0f09bfc02..e66104062 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/files/FileViewer.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/files/FileViewer.svelte @@ -4,8 +4,6 @@ import ImageViewer from './ImageViewer.svelte'; import MarkdownViewer from './MarkdownViewer.svelte'; import CodeViewer from './CodeViewer.svelte'; - import CodeBlock from "$lib/components/codeblock/CodeBlock.svelte"; - import { Code } from "lucide-svelte"; /** * Main file viewer component that routes to appropriate specialized viewers diff --git a/crates/opsml_server/opsml_ui/src/lib/components/files/fileTreeLoader.ts b/crates/opsml_server/opsml_ui/src/lib/components/files/fileTreeLoader.ts new file mode 100644 index 000000000..a9e68afc9 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/files/fileTreeLoader.ts @@ -0,0 +1,42 @@ +import { getFileTree } from "$lib/server/card/files/utils"; +import { getRegistryPath, getRegistryTableName } from "$lib/utils"; + +/** + * Loads file tree and navigation metadata for a card file route. + * Encapsulates slug parsing, registry path construction, and file tree retrieval. + * @param parent - SvelteKit parent loader function + * @param params - Route parameters + * @param fetch - SvelteKit fetch function + * @returns File tree, previous path, and root indicator + */ +export async function loadFileTree({ + parent, + params, + fetch, +}: { + parent: () => Promise<{ metadata: any; registryType: string }>; + params: { file: string }; + fetch: typeof globalThis.fetch; +}) { + const slug = params.file as string; + const slugs = slug.split("/"); + + const { metadata, registryType } = await parent(); + + // @ts-ignore + const tableName = getRegistryTableName(registryType); + let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; + + // @ts-ignore + const previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ + metadata.space + }/${metadata.name}/${metadata.version}/files/${slugs + .slice(0, slugs.length - 1) + .join("/")}`.replace(/\/$/, ""); + + basePath = `${basePath}/${slugs.join("/")}`; + + const fileTree = await getFileTree(fetch, basePath); + + return { fileTree, previousPath, isRoot: false }; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/files/utils.ts b/crates/opsml_server/opsml_ui/src/lib/components/files/utils.ts index b4857394f..e8f4e7b9f 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/files/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/files/utils.ts @@ -1,22 +1,4 @@ -import { opsmlClient } from "$lib/components/api/client.svelte"; -import { RoutePaths } from "$lib/components/api/routes"; -import type { FileTreeResponse, RawFile, RawFileRequest } from "./types"; import { AcceptableSuffix } from "./types"; -import { userStore } from "../user/user.svelte"; -import type { RegistryType } from "$lib/utils"; - -export async function getFileTree(path: string): Promise { - const params = { - path: path, - }; - - const response = await opsmlClient.get( - RoutePaths.FILE_TREE, - params, - userStore.jwt_token - ); - return (await response.json()) as FileTreeResponse; -} export function timeAgo(timestamp: string): string { let date: Date; @@ -115,25 +97,6 @@ export function isAcceptableSuffix(suffix: string): boolean { ); } -export async function getRawFile( - path: string, - uid: string, - registry_type: RegistryType -): Promise { - const body: RawFileRequest = { - path: path, - uid: uid, - registry_type: registry_type, - }; - - const response = await opsmlClient.post( - RoutePaths.FILE_CONTENT, - body, - userStore.jwt_token - ); - return (await response.json()) as RawFile; -} - function splitViewPath(path: string): string[] { let splitPath = path.split("/"); return splitPath; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/genai/mock.ts b/crates/opsml_server/opsml_ui/src/lib/components/genai/mock.ts new file mode 100644 index 000000000..d5e642175 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/genai/mock.ts @@ -0,0 +1,95 @@ +import { Provider, ResponseType, Role, type Prompt } from "./types"; +import type { OpenAIChatSettings } from "./settings/openai"; +import type { GeminiSettings } from "./settings/gemini"; + +/** + * Mock OpenAI Prompt + */ +export const mockOpenAIPrompt: Prompt = { + message: [ + { + content: { Str: "What is the capital of France?" }, + role: Role.User, + variables: [], + }, + { + content: { Str: "The capital of France is Paris." }, + role: Role.Assistant, + variables: [], + }, + ], + system_instruction: [ + { + content: { Str: "You are a helpful assistant." }, + role: Role.Assistant || "system", + variables: [], + }, + ], + model_settings: { + OpenAIChat: { + max_completion_tokens: 128, + temperature: 0.7, + top_p: 1, + stop_sequences: ["\n"], + audio: { format: "mp3", voice: "en-US-Wavenet-D" }, + tool_choice: { mode: "auto" }, + tools: [], + metadata: { project: "demo" }, + } as OpenAIChatSettings, + }, + model: "gpt-4", + provider: Provider.OpenAI, + version: "1.0.0", + response_json_schema: undefined, + parameters: ["capital", "country"], + response_type: ResponseType.Pydantic, +}; + +/** + * Mock Gemini Prompt + */ +export const mockGeminiPrompt: Prompt = { + message: [ + { + content: { Str: "Summarize the attached document." }, + role: Role.User, + variables: [], + }, + { + content: { Str: "Here is the summary of your document..." }, + role: Role.Assistant, + variables: [], + }, + ], + system_instruction: [ + { + content: { Str: "You are an expert summarizer." }, + role: Role.System || "system", + variables: [], + }, + ], + model_settings: { + GoogleChat: { + labels: { project: "demo-gemini" }, + generation_config: { + temperature: 0.5, + top_p: 0.9, + max_output_tokens: 256, + stop_sequences: [""], + response_modalities: ["Text"], + }, + safety_settings: [ + { + category: "HarmCategoryUnspecified", + threshold: "BlockNone", + }, + ], + } as GeminiSettings, + }, + model: "gemini-pro", + provider: Provider.Gemini, + version: "1.0.0", + response_json_schema: undefined, + parameters: ["summary", "document"], + response_type: ResponseType.Score, +}; diff --git a/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/gemini.ts b/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/gemini.ts new file mode 100644 index 000000000..9741a5d5e --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/gemini.ts @@ -0,0 +1,196 @@ +/** + * Gemini model settings and supporting types. + * Strictly typed for SSR, hydration, and maintainability in SvelteKit. + */ + +export enum SchemaType { + TypeUnspecified = "TypeUnspecified", + String = "String", + Number = "Number", + Integer = "Integer", + Boolean = "Boolean", + Array = "Array", + Object = "Object", + Null = "Null", +} + +export interface Schema { + type?: SchemaType; + format?: string; + title?: string; + description?: string; + nullable?: boolean; + default?: unknown; + items?: Schema; + min_items?: string; + max_items?: string; + enum?: string[]; + properties?: Record; + property_ordering?: string[]; + required?: string[]; + min_properties?: string; + max_properties?: string; + minimum?: number; + maximum?: number; + min_length?: string; + max_length?: string; + pattern?: string; + example?: unknown; + any_of?: Schema[]; + additional_properties?: unknown; + ref_path?: string; + defs?: Record; +} + +export enum HarmCategory { + HarmCategoryUnspecified = "HarmCategoryUnspecified", + HarmCategoryHateSpeech = "HarmCategoryHateSpeech", + HarmCategoryDangerousContent = "HarmCategoryDangerousContent", + HarmCategoryHarassment = "HarmCategoryHarassment", + HarmCategorySexuallyExplicit = "HarmCategorySexuallyExplicit", + HarmCategoryImageHate = "HarmCategoryImageHate", + HarmCategoryImageDangerousContent = "HarmCategoryImageDangerousContent", + HarmCategoryImageHarassment = "HarmCategoryImageHarassment", + HarmCategoryImageSexuallyExplicit = "HarmCategoryImageSexuallyExplicit", +} + +export enum HarmBlockThreshold { + HarmBlockThresholdUnspecified = "HarmBlockThresholdUnspecified", + BlockLowAndAbove = "BlockLowAndAbove", + BlockMediumAndAbove = "BlockMediumAndAbove", + BlockOnlyHigh = "BlockOnlyHigh", + BlockNone = "BlockNone", + Off = "Off", +} + +export enum HarmBlockMethod { + HarmBlockMethodUnspecified = "HarmBlockMethodUnspecified", + Severity = "Severity", + Probability = "Probability", +} + +export interface SafetySetting { + category: HarmCategory; + threshold: HarmBlockThreshold; + method?: HarmBlockMethod; +} + +export enum Modality { + ModalityUnspecified = "ModalityUnspecified", + Text = "Text", + Image = "Image", + Audio = "Audio", +} + +export enum MediaResolution { + MediaResolutionUnspecified = "MediaResolutionUnspecified", + MediaResolutionLow = "MediaResolutionLow", + MediaResolutionMedium = "MediaResolutionMedium", + MediaResolutionHigh = "MediaResolutionHigh", +} + +export enum ModelRoutingPreference { + Unknown = "Unknown", + PrioritizeQuality = "PrioritizeQuality", + Balanced = "Balanced", + PrioritizeCost = "PrioritizeCost", +} + +export interface ThinkingConfig { + include_thoughts?: boolean; + thinking_budget?: number; +} + +export interface AutoRoutingMode { + model_routing_preference?: ModelRoutingPreference; +} + +export interface ManualRoutingMode { + model_name: string; +} + +export type RoutingConfigMode = AutoRoutingMode | ManualRoutingMode; + +export interface RoutingConfig { + routing_config: RoutingConfigMode; +} + +export interface PrebuiltVoiceConfig { + voice_name: string; +} + +export type VoiceConfigMode = PrebuiltVoiceConfig; + +export interface VoiceConfig { + voice_config: VoiceConfigMode; +} + +export interface SpeechConfig { + voice_config?: VoiceConfig; + language_code?: string; +} + +export interface GenerationConfig { + stop_sequences?: string[]; + response_mime_type?: string; + response_modalities?: Modality[]; + thinking_config?: ThinkingConfig; + temperature?: number; + top_p?: number; + top_k?: number; + candidate_count?: number; + max_output_tokens?: number; + response_logprobs?: boolean; + logprobs?: number; + presence_penalty?: number; + frequency_penalty?: number; + seed?: number; + response_schema?: Schema; + response_json_schema?: unknown; + routing_config?: RoutingConfig; + audio_timestamp?: boolean; + media_resolution?: MediaResolution; + speech_config?: SpeechConfig; + enable_affective_dialog?: boolean; +} + +export interface ModelArmorConfig { + prompt_template_name?: string; + response_template_name?: string; +} + +export enum Mode { + ModeUnspecified = "ModeUnspecified", + Any = "Any", + Auto = "Auto", + None = "None", +} + +export interface FunctionCallingConfig { + mode?: Mode; + allowed_function_names?: string[]; +} + +export interface LatLng { + latitude: number; + longitude: number; +} + +export interface RetrievalConfig { + lat_lng: LatLng; + language_code: string; +} + +export interface ToolConfig { + function_calling_config?: FunctionCallingConfig; + retrieval_config?: RetrievalConfig; +} + +export interface GeminiSettings { + labels?: Record; + tool_config?: ToolConfig; + generation_config?: GenerationConfig; + safety_settings?: SafetySetting[]; + model_armor_config?: ModelArmorConfig; + extra_body?: unknown; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/openai.ts b/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/openai.ts new file mode 100644 index 000000000..d737559d0 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/genai/settings/openai.ts @@ -0,0 +1,166 @@ +/** + * OpenAIChatSettings and supporting types. + * Strictly typed for SvelteKit SSR and hydration. + */ + +// Audio parameter settings for model +export interface AudioParam { + format: string; + voice: string; +} + +// Content part for multi-part content +export interface ContentPart { + type: string; + text: string; +} + +// Content can be a simple string or an array of parts +export type Content = string | ContentPart[]; + +// Prediction output type +export interface Prediction { + type: string; + content: Content; +} + +// Streaming options for model output +export interface StreamOptions { + include_obfuscation?: boolean; + include_usage?: boolean; +} + +// Tool choice modes +export type ToolChoiceMode = "none" | "auto" | "required"; + +// Function tool choice specification +export interface FunctionChoice { + name: string; +} + +// Function tool choice object +export interface FunctionToolChoice { + type: "function"; + function: FunctionChoice; +} + +// Custom tool choice specification +export interface CustomChoice { + name: string; +} + +// Custom tool choice object +export interface CustomToolChoice { + type: "custom"; + custom: CustomChoice; +} + +// Tool definition for allowed tools +export interface ToolDefinition { + type: "function"; + function: FunctionChoice; +} + +// Allowed tools mode +export type AllowedToolsMode = "auto" | "required"; + +// Inner allowed tools configuration +export interface InnerAllowedTools { + mode: AllowedToolsMode; + tools: ToolDefinition[]; +} + +// Allowed tools configuration +export interface AllowedTools { + type: "allowed_tools"; + allowed_tools: InnerAllowedTools; +} + +// Tool choice union +export type ToolChoice = + | { mode: ToolChoiceMode } + | FunctionToolChoice + | CustomToolChoice + | AllowedTools; + +// Function definition for tool +export interface FunctionDefinition { + name: string; + description?: string; + parameters?: unknown; + strict?: boolean; +} + +// Function tool for model +export interface FunctionTool { + function: FunctionDefinition; + type: "function"; +} + +// Text format for custom tool +export interface TextFormat { + type: string; +} + +// Grammar format for custom tool +export interface Grammar { + definition: string; + syntax: string; +} + +export interface GrammarFormat { + grammar: Grammar; + type: string; +} + +// Custom tool format union +export type CustomToolFormat = TextFormat | GrammarFormat; + +// Custom tool definition +export interface CustomDefinition { + name: string; + description?: string; + format?: CustomToolFormat; +} + +// Custom tool for model +export interface CustomTool { + custom: CustomDefinition; + type: "custom"; +} + +// Tool union +export type Tool = FunctionTool | CustomTool; + +// OpenAIChatSettings main interface +export interface OpenAIChatSettings { + max_completion_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + frequency_penalty?: number; + timeout?: number; + parallel_tool_calls?: boolean; + seed?: number; + logit_bias?: Record; + stop_sequences?: string[]; + logprobs?: boolean; + audio?: AudioParam; + metadata?: Record; + modalities?: string[]; + n?: number; + prediction?: Prediction; + presence_penalty?: number; + prompt_cache_key?: string; + reasoning_effort?: string; + safety_identifier?: string; + service_tier?: string; + store?: boolean; + stream?: boolean; + stream_options?: StreamOptions; + tool_choice?: ToolChoice; + tools?: Tool[]; + top_logprobs?: number; + verbosity?: string; + extra_body?: unknown; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/genai/types.ts b/crates/opsml_server/opsml_ui/src/lib/components/genai/types.ts new file mode 100644 index 000000000..493f9f260 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/components/genai/types.ts @@ -0,0 +1,107 @@ +import type { GeminiSettings } from "./settings/gemini"; +import type { OpenAIChatSettings } from "./settings/openai"; + +/** + * Provider enum for model sources. + */ +export enum Provider { + OpenAI = "OpenAI", + Gemini = "Gemini", + Google = "Google", + Vertex = "Vertex", + Undefined = "Undefined", +} + +/** + * ResponseType enum for prompt responses. + */ +export enum ResponseType { + Score = "Score", + Pydantic = "Pydantic", + Null = "Null", +} + +/** + * Audio URL type for audio content. + */ +export interface AudioUrl { + url: string; + kind: "audio-url"; +} + +/** + * Image URL type for image content. + */ +export interface ImageUrl { + url: string; + kind: "image-url"; +} + +/** + * Document URL type for document content. + */ +export interface DocumentUrl { + url: string; + kind: "document-url"; +} + +/** + * Binary content type for arbitrary media. + */ +export interface BinaryContent { + data: Uint8Array; + media_type: string; + kind: "binary"; +} + +/** + * PromptContent union for all supported content types. + */ +export type PromptContent = + | { Str: string } + | { Audio: AudioUrl } + | { Image: ImageUrl } + | { Document: DocumentUrl } + | { Binary: BinaryContent }; + +/** + * Role enum for message sender/receiver. + */ +export enum Role { + User = "user", + Assistant = "assistant", + Developer = "developer", + Tool = "tool", + Model = "model", +} + +/** + * Message structure for prompt conversations. + */ +export interface Message { + content: PromptContent; + role: Role | string; + variables: string[]; +} + +/** + * ModelSettings union for supported providers. + */ +export type ModelSettings = + | { OpenAIChat: OpenAIChatSettings } + | { GoogleChat: GeminiSettings }; + +/** + * Main Prompt interface for LLM requests. + */ +export interface Prompt { + message: Message[]; + system_instruction: Message[]; + model_settings: ModelSettings; + model: string; + provider: Provider; + version: string; + response_json_schema?: unknown; + parameters: string[]; + response_type: ResponseType; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/home/HomeCard.svelte b/crates/opsml_server/opsml_ui/src/lib/components/home/HomeCard.svelte index ec08a92a2..8d1748f88 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/home/HomeCard.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/home/HomeCard.svelte @@ -1,7 +1,6 @@ diff --git a/crates/opsml_server/opsml_ui/src/lib/components/user/UserDropdown.svelte b/crates/opsml_server/opsml_ui/src/lib/components/user/UserDropdown.svelte index 89e096450..68db99789 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/user/UserDropdown.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/user/UserDropdown.svelte @@ -1,5 +1,6 @@ + +
+ +
+ + + +
+ +
+ {#each filtered as option (option)} +
+ {option} + {#if combobox.isSelected(option)} + ✓ + {/if} +
+ {:else} + No results found + {/each} +
+
+ diff --git a/crates/opsml_server/opsml_ui/src/lib/components/utils/LinkPill.svelte b/crates/opsml_server/opsml_ui/src/lib/components/utils/LinkPill.svelte index 4cf226ea5..55e01cfa1 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/utils/LinkPill.svelte +++ b/crates/opsml_server/opsml_ui/src/lib/components/utils/LinkPill.svelte @@ -1,8 +1,9 @@ + +
+
+ + +
+ +
+ {#each availableItems as option (option)} +
+ {option} + {#if combobox.isSelected(option)} + ✓ + {/if} +
+ {:else} + No results found + {/each} +
+
+ {#each filteredItems as item} + + {/each} +
+
+ diff --git a/crates/opsml_server/opsml_ui/src/lib/components/viz/chart.ts b/crates/opsml_server/opsml_ui/src/lib/components/viz/chart.ts index a2cff3c22..552101a0b 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/viz/chart.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/viz/chart.ts @@ -149,25 +149,36 @@ export function createLineChart( ): ChartConfiguration { const datasets: ChartjsLineDataset[] = []; + const metricNames = Object.keys(metricData); const uniqueVersions = new Set( Object.values(metricData) .flat() .map((metric) => metric.version) ); - // Generate colors based on number of unique experiments - const colors = generateColors(uniqueVersions.size, 1.0); + let colors: string[]; + let getColorIndex: (metricName: string, version: string) => number; - // Create version to color index mapping - const versionColorMap = new Map( - Array.from(uniqueVersions).map((version, index) => [version, index]) - ); + if (uniqueVersions.size === 1) { + // Only one version: color by metric name + colors = generateColors(metricNames.length, 1.0); + const metricColorMap = new Map(metricNames.map((name, idx) => [name, idx])); + getColorIndex = (metricName) => metricColorMap.get(metricName) ?? 0; + } else { + // Multiple versions: color by version + colors = generateColors(uniqueVersions.size, 1.0); + const versionColorMap = new Map( + Array.from(uniqueVersions).map((version, idx) => [version, idx]) + ); + getColorIndex = (_metricName, version) => versionColorMap.get(version) ?? 0; + } - // For each metric name Object.entries(metricData).forEach(([metricName, groupedMetrics]) => { - // For each experiment's grouped metrics groupedMetrics.forEach((metric) => { - const colorIndex = versionColorMap.get(metric.version) ?? 0; + const colorIndex = + uniqueVersions.size === 1 + ? getColorIndex(metricName, metric.version) + : getColorIndex(metricName, metric.version); datasets.push({ label: `${metricName} - ${metric.version}`, @@ -182,24 +193,18 @@ export function createLineChart( }); }); - // Get first non-empty metric data for x-axis values const firstMetric = Object.values(metricData)[0]?.[0]; - - // Try steps first, then timestamp, then fallback to array indices const xValues = Array.from( firstMetric?.step ?? firstMetric?.timestamp ?? Array.from({ length: firstMetric?.value.length ?? 0 }, (_, i) => i) ); - - // Adjust x-axis label based on what we're using const effectiveXLabel = firstMetric?.step ? "Step" : firstMetric?.timestamp ? "Time" : "Index"; - // Build the line chart configuration if effectiveXLabel is Time if (effectiveXLabel === "Time") { const xAsDate = xValues.map((x) => new Date(x)); return buildTimeChart( @@ -227,36 +232,66 @@ export function createGroupedBarChart( .map((metric) => metric.version) ); - const borderColors = generateColors(uniqueVersions.size, 1.0); - const backgroundColors = generateColors(uniqueVersions.size, 0.5); - - const versionColorMap = new Map( - Array.from(uniqueVersions).map((version, index) => [version, index]) - ); - - // For each metric name - Array.from(uniqueVersions).forEach((version) => { - const values: number[] = []; + if (uniqueVersions.size === 1) { + // Only one version: color each metric differently + const borderColors = generateColors(metricNames.length, 1.0); + const backgroundColors = generateColors(metricNames.length, 0.5); + const version = Array.from(uniqueVersions)[0]; - // For each metric, find the value for this version - metricNames.forEach((metricName) => { - const metric = metricData[metricName].find((m) => m.version === version); + metricNames.forEach((metricName, idx) => { + const metric = metricData[metricName][0]; const value = metric ? Array.from(metric.value).pop() ?? 0 : 0; - values.push(value); + + datasets.push({ + label: metricName, + data: [value], + backgroundColor: backgroundColors[idx], + borderColor: borderColors[idx], + borderWidth: 2, + }); }); - const colorIndex = versionColorMap.get(version) ?? 0; + return buildChart([version], datasets, "Version", y_label, true, "bar"); + } else { + // Multiple versions: create one dataset per version, color by version + const borderColors = generateColors(uniqueVersions.size, 1.0); + const backgroundColors = generateColors(uniqueVersions.size, 0.5); - datasets.push({ - label: version, - data: values, - backgroundColor: backgroundColors[colorIndex], - borderColor: borderColors[colorIndex], - borderWidth: 2, + const versionColorMap = new Map( + Array.from(uniqueVersions).map((version, index) => [version, index]) + ); + + Array.from(uniqueVersions).forEach((version) => { + const values: number[] = []; + + metricNames.forEach((metricName) => { + const metric = metricData[metricName].find( + (m) => m.version === version + ); + const value = metric ? Array.from(metric.value).pop() ?? 0 : 0; + values.push(value); + }); + + const colorIndex = versionColorMap.get(version) ?? 0; + + datasets.push({ + label: version, + data: values, + backgroundColor: backgroundColors[colorIndex], + borderColor: borderColors[colorIndex], + borderWidth: 2, + }); }); - }); - return buildChart(metricNames, datasets, "Experiments", y_label, true, "bar"); + return buildChart( + metricNames, + datasets, + "Experiments", + y_label, + true, + "bar" + ); + } } export function createBarChart( diff --git a/crates/opsml_server/opsml_ui/src/lib/components/viz/timeseries.ts b/crates/opsml_server/opsml_ui/src/lib/components/viz/timeseries.ts index 8cb04f09f..779fdb1c4 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/viz/timeseries.ts +++ b/crates/opsml_server/opsml_ui/src/lib/components/viz/timeseries.ts @@ -29,17 +29,6 @@ export function buildTimeChart( const maxY = Math.max(...allYValues); const minY = Math.min(...allYValues); - let yMin: number; - let yMax: number; - - if (maxY === minY) { - yMin = minY - 1; - yMax = maxY + 1; - } else { - yMin = minY - Math.abs(minY) * 0.1; - yMax = maxY + Math.abs(maxY) * 0.1; - } - const annotation = typeof baselineValue === "number" ? { @@ -154,8 +143,8 @@ export function buildTimeChart( }, }, y: { - min: yMin, - max: yMax, + suggestedMin: minY - Math.abs(minY) * 0.1, + suggestedMax: maxY + Math.abs(maxY) * 0.1, title: { display: true, text: y_label, diff --git a/crates/opsml_server/opsml_ui/src/lib/server/api/opsmlClient.ts b/crates/opsml_server/opsml_ui/src/lib/server/api/opsmlClient.ts new file mode 100644 index 000000000..407818d40 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/api/opsmlClient.ts @@ -0,0 +1,172 @@ +import { goto } from "$app/navigation"; +import { browser } from "$app/environment"; +import { redirect } from "@sveltejs/kit"; + +/** + * OpsmlClient is a wrapper around the Fetch API to interact with the OpsML axum backend. + * It provides methods for GET, POST, PUT, DELETE, and PATCH requests with built-in + * error handling and token management. + */ +export class OpsmlClient { + private fetchFn: typeof globalThis.fetch; + + constructor(fetchFn: typeof globalThis.fetch = fetch) { + this.fetchFn = fetchFn; + } + + private getBaseUrl(): string { + // Use OPSML_SERVER_PORT from process.env or fallback to 8080 + const port = process.env.OPSML_SERVER_PORT ?? "8080"; + return `http://localhost:${port}`; + } + + /** + * Handles API errors and redirects on 401. + * @param error - Error object or message + * @param status - Optional HTTP status code + */ + private handleError(error: any, status?: number) { + console.error("API Error:", error); + + // Only use goto() on the client side + if (browser && status === 401) { + goto("/opsml/user/login"); + } else if (!browser && status === 401) { + // Use redirect for server-side + throw redirect(303, "/opsml/user/login"); + } + } + + /** + * Adds query parameters to a URL. This is a special param handler to ensure + * compatibility with how the axum backend expects arrays and objects. + * @param url - URL object + * @param params - Query params as key-value pairs + */ + private addQueryParams(url: URL, params?: Record): URL { + if (!params) return url; + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if ( + Array.isArray(value) || + (typeof value === "object" && typeof value.length === "number") + ) { + for (let i = 0; i < value.length; i++) { + url.searchParams.append(`${key}[${i}]`, String(value[i])); + } + } else if (typeof value === "object") { + url.searchParams.append(key, JSON.stringify(value)); + } else { + url.searchParams.append(key, String(value)); + } + } + }); + return url; + } + + /** + * Generic request handler for all HTTP methods. + * @param path - API path (relative) + * @param options - Fetch options + */ + async request(path: string, options: RequestInit = {}): Promise { + const baseUrl = this.getBaseUrl(); + const url = new URL(path, baseUrl); + + try { + const response = await this.fetchFn(url.toString(), { + ...options, + headers: options.headers, + }); + + if (!response.ok) { + this.handleError(`HTTP ${response.status}`, response.status); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response; + } catch (error) { + this.handleError(error); + throw error; + } + } + + /** + * GET request with optional query params and bearer token. + */ + async get(path: string, params?: Record): Promise { + const url = this.addQueryParams(new URL(path, this.getBaseUrl()), params); + const headers: Record = {}; + return this.request(url.pathname + url.search, { method: "GET", headers }); + } + + async validateToken(path: string, token: string): Promise { + const headers: Record = { + Authorization: `Bearer ${token}`, + }; + return this.request(path, { method: "GET", headers }); + } + + async refreshToken(path: string, token: string): Promise { + const headers: Record = { + Authorization: `Bearer ${token}`, + }; + return this.request(path, { method: "POST", headers }); + } + + /** + * DELETE request with optional body and bearer token. + */ + async delete(path: string, body?: any): Promise { + const headers: Record = {}; + return this.request(path, { + method: "DELETE", + headers: { "Content-Type": "application/json", ...headers }, + body: body ? JSON.stringify(body) : undefined, + }); + } + + /** + * PUT request with body and optional bearer token. + */ + async put(path: string, body: any): Promise { + const headers: Record = {}; + return this.request(path, { + method: "PUT", + headers: { "Content-Type": "application/json", ...headers }, + body: JSON.stringify(body), + }); + } + + /** + * PATCH request with body and optional bearer token. + */ + async patch(path: string, body: any): Promise { + const headers: Record = {}; + return this.request(path, { + method: "PATCH", + headers: { "Content-Type": "application/json", ...headers }, + body: JSON.stringify(body), + }); + } + + async post( + path: string, + body: any, + additionalHeaders: Record = {} + ): Promise { + return this.request(path, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...additionalHeaders, + }, + body: JSON.stringify(body), + }); + } +} + +export function createOpsmlClient(fetch: typeof globalThis.fetch) { + const client = new OpsmlClient(fetch); + return client; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/auth/validateToken.ts b/crates/opsml_server/opsml_ui/src/lib/server/auth/validateToken.ts new file mode 100644 index 000000000..c57acb430 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/auth/validateToken.ts @@ -0,0 +1,204 @@ +import { createOpsmlClient, OpsmlClient } from "../api/opsmlClient"; +import { RoutePaths, UiPaths } from "$lib/components/api/routes"; +import { redirect, type Cookies } from "@sveltejs/kit"; +import type { + AuthenticatedResponse, + JwtToken, + LoginResponse, +} from "../../components/user/types"; + +/** + * Validates JWT token from cookies, attempts refresh if expired, + * and redirects to login if invalid or refresh fails. + * Flow: + * 1. Check for JWT token in cookies + * 2. Validate token with server + * 3. If expired, attempt to refresh token + * 4. If refresh successful, set new token in cookies and re-validate + * 5. If all fails, clear cookie and redirect to login + * + * @param cookies - SvelteKit cookies object + * @param redirectPath - Path to redirect if validation fails (default: UiPaths.LOGIN) + */ +export async function validateTokenOrRedirect( + cookies: Cookies, + redirectPath: string = UiPaths.LOGIN +): Promise { + const jwtToken = cookies.get("jwt_token"); + const opsmlClient = createOpsmlClient(fetch); + + // Validate the JWT token + const validationResult = await validateJWTToken(opsmlClient, jwtToken ?? ""); + + if (validationResult.isValid) return; // Authenticated + + // If token is present and expired, attempt to refresh + if (jwtToken && isTokenExpired(jwtToken)) { + const refreshedToken = await attemptRefreshToken(opsmlClient, jwtToken); + if (refreshedToken) { + // Set new token in cookies and validate again + await setTokenInCookies(cookies, refreshedToken); + const refreshedValidation = await validateJWTToken( + opsmlClient, + refreshedToken + ); + if (refreshedValidation.isValid) return; + } + } + // If all else fails, clear cookie and redirect + cookies.delete("jwt_token", { path: "/" }); + throw redirect(303, redirectPath); +} + +/** + * Attempts to refresh the JWT token via backend. + */ +async function attemptRefreshToken( + opsmlClient: OpsmlClient, + jwt_Token: string +): Promise { + try { + const response = await opsmlClient.refreshToken( + RoutePaths.REFRESH_TOKEN, + jwt_Token + ); + if (!response.ok) return null; + const jwtToken = (await response.json()) as JwtToken; + return jwtToken.token ?? null; + } catch { + return null; + } +} + +interface AuthValidationResult { + isValid: boolean; + shouldRedirect: boolean; + error?: string; +} + +/** + * Validates JWT token against the server + * @param jwtToken - The JWT token to validate + * @returns Promise with validation result + */ +async function validateJWTToken( + opsmlClient: OpsmlClient, + jwt_token: string +): Promise { + try { + const response = await opsmlClient.validateToken( + RoutePaths.VALIDATE_AUTH, + jwt_token + ); + + if (!response.ok) { + const error = `Authentication failed: ${response.status} ${response.statusText}`; + console.error(error); + return { + isValid: false, + shouldRedirect: response.status === 401 || response.status === 403, + error, + }; + } + + const authenticated = (await response.json()) as AuthenticatedResponse; + + return { + isValid: authenticated.is_authenticated, + shouldRedirect: !authenticated.is_authenticated, + error: authenticated.is_authenticated + ? undefined + : "User not authenticated", + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown validation error"; + console.error("JWT validation error:", errorMessage); + + return { + isValid: false, + shouldRedirect: true, + error: errorMessage, + }; + } +} + +/** + * Logs in a user on the server and returns the login response + * @param username - The username + * @param password - The password + * @returns Promise with login response + */ +export async function loginUser( + fetch: typeof globalThis.fetch, + username: string, + password: string +): Promise { + const opsmlClient = createOpsmlClient(fetch); + const response = await opsmlClient.post(RoutePaths.LOGIN, { + username, + password, + }); + + const data = (await response.json()) as LoginResponse; + return data; +} + +/** + * Sets the JWT token in an HttpOnly cookie with domain, expiration, and path. + * This should be used server-side for secure authentication. + * + * @param cookies - SvelteKit cookies object + * @param token - JWT token string + * @param options - Optional overrides for domain, expiration (ms), and path + */ +export async function setTokenInCookies( + cookies: Cookies, + token: string, + options?: { + domain?: string; + expiresMs?: number; + path?: string; + } +): Promise { + const expirationDate = new Date(); + expirationDate.setTime( + expirationDate.getTime() + (options?.expiresMs ?? 60 * 60 * 1000) // default: 1 hour + ); + + cookies.set("jwt_token", token, { + httpOnly: true, + secure: true, + sameSite: "lax", + domain: options?.domain ?? "localhost", + expires: expirationDate, + path: options?.path ?? "/", + }); +} + +export async function setUsernameInCookies( + cookies: Cookies, + username: string +): Promise { + cookies.set("username", username, { + httpOnly: false, + secure: true, + sameSite: "lax", + domain: "localhost", + path: "/", + }); +} + +interface JwtPayload { + exp: number; + [key: string]: unknown; +} + +export function isTokenExpired(token: string): boolean { + try { + const payload = JSON.parse(atob(token.split(".")[1])) as JwtPayload; + return payload.exp * 1000 < Date.now(); + } catch { + return true; + } +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/card/files/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/card/files/utils.ts new file mode 100644 index 000000000..40dc7e2ed --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/card/files/utils.ts @@ -0,0 +1,41 @@ +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; +import { RoutePaths } from "$lib/components/api/routes"; +import type { + FileTreeResponse, + RawFile, + RawFileRequest, +} from "$lib/components/files/types"; +import type { RegistryType } from "$lib/utils"; + +export async function getFileTree( + fetch: typeof globalThis.fetch, + path: string +): Promise { + const params = { + path: path, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.FILE_TREE, + params + ); + return (await response.json()) as FileTreeResponse; +} +export async function getRawFile( + fetch: typeof globalThis.fetch, + path: string, + uid: string, + registry_type: RegistryType +): Promise { + const body: RawFileRequest = { + path: path, + uid: uid, + registry_type: registry_type, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.FILE_CONTENT, + body + ); + return (await response.json()) as RawFile; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/card/layout.ts b/crates/opsml_server/opsml_ui/src/lib/server/card/layout.ts new file mode 100644 index 000000000..bc6511221 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/card/layout.ts @@ -0,0 +1,32 @@ +import { getCardMetadata } from "./utils"; +import { getCardReadMe } from "./readme/utils"; +import type { RegistryType } from "$lib/utils"; +import type { ReadMe } from "$lib/components/readme/util"; + +/** + * Loads card layout data + */ +export async function loadCardLayout( + registryType: RegistryType, + space: string, + name: string, + version: string, + fetch: typeof globalThis.fetch +): Promise<{ + metadata: any; + registryType: RegistryType; + readme: ReadMe; + activeTab: string; +}> { + const [metadata, readme] = await Promise.all([ + getCardMetadata(space, name, version, undefined, registryType, fetch), + getCardReadMe(name, space, registryType, fetch), + ]); + + return { + metadata, + registryType, + readme, + activeTab: "card", + }; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/card/monitoring/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/card/monitoring/utils.ts new file mode 100644 index 000000000..40a2db4e6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/card/monitoring/utils.ts @@ -0,0 +1,193 @@ +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; +import { RoutePaths } from "$lib/components/api/routes"; +import { + type UpdateProfileRequest, + type UpdateResponse, + type LLMPageRequest, + type LLMPageResponse, + type PaginationCursor, + type ServiceInfo, + type LLMPaginationRequest, + type TimeInterval, + Status, + DriftType, + type DriftRequest, + type BinnedDriftMap, +} from "$lib/components/card/monitoring/types"; +import { RegistryType } from "$lib/utils"; +import type { DriftProfileUri } from "$lib/components/card/monitoring/types"; +import type { DriftProfileResponse } from "$lib/components/card/monitoring/utils"; +import type { + AlertResponse, + DriftAlertRequest, + Alert, + UpdateAlertResponse, + UpdateAlertStatus, +} from "$lib/components/card/monitoring/alert/types"; +import { + timeIntervalToDateTime, + getProfileConfig, +} from "$lib/components/card/monitoring/utils"; + +export async function getDriftProfiles( + fetch: typeof globalThis.fetch, + uid: string, + driftMap: Record, + registryType: RegistryType +): Promise { + const body = { + uid: uid, + drift_profile_uri_map: driftMap, + registry_type: registryType, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.DRIFT_PROFILE_UI, + body + ); + return (await response.json()) as DriftProfileResponse; +} + +export async function getLLMRecordPage( + fetch: typeof globalThis.fetch, + service_info: ServiceInfo, + status?: Status, + cursor?: PaginationCursor +): Promise { + let pagination: LLMPaginationRequest = { + cursor, + limit: 20, + }; + + const request: LLMPageRequest = { + service_info, + status, + pagination, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.LLM_RECORD_PAGE, + request + ); + return (await response.json()) as LLMPageResponse; +} + +export async function updateDriftProfile( + fetch: typeof globalThis.fetch, + updateRequest: UpdateProfileRequest +): Promise { + const response = await createOpsmlClient(fetch).put( + RoutePaths.DRIFT_PROFILE, + updateRequest + ); + return (await response.json()) as UpdateResponse; +} + +export async function getDriftAlerts( + fetch: typeof globalThis.fetch, + space: string, + name: string, + version: string, + timeInterval: TimeInterval, + active: boolean +): Promise { + // For testing purposes, return sample alerts + let alertRequest: DriftAlertRequest = { + space: space, + name: name, + version: version, + limit_datetime: timeIntervalToDateTime(timeInterval), + active: active, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.DRIFT_ALERT, + alertRequest + ); + if (!response.ok) { + throw new Error(`Failed to fetch drift alerts: ${response.status}`); + } + const alertResponse = (await response.json()) as AlertResponse; + + return alertResponse.alerts; +} + +//// Acknowledge an alert by its ID +export async function acknowledgeAlert( + fetch: typeof globalThis.fetch, + id: number, + space: string +): Promise { + const request: UpdateAlertStatus = { + id: id, + active: false, + space: space, + }; + + const response = await createOpsmlClient(fetch).put( + RoutePaths.DRIFT_ALERT, + request + ); + + if (!response.ok) { + throw new Error(`Failed to acknowledge alert: ${response.status}`); + } + const updateResponse = (await response.json()) as UpdateAlertResponse; + + if (!updateResponse.updated) { + throw new Error("Failed to acknowledge alert"); + } + + return updateResponse.updated; +} + +export async function getLatestMetrics( + fetch: typeof globalThis.fetch, + profiles: DriftProfileResponse, + time_interval: TimeInterval, + max_data_points: number +): Promise { + const driftMap: BinnedDriftMap = {}; + + // Create an array of promises + const requests = Object.entries(profiles).map( + async ([driftType, profile]) => { + const config = getProfileConfig(driftType as DriftType, profile.profile); + + const request: DriftRequest = { + name: config.name, + space: config.space, + version: config.version, + time_interval: time_interval, + max_data_points: max_data_points, + drift_type: driftType as DriftType, + }; + + // Determine route based on drift type + const route = (() => { + switch (driftType) { + case DriftType.Custom: + return RoutePaths.CUSTOM_DRIFT; + case DriftType.Psi: + return RoutePaths.PSI_DRIFT; + case DriftType.Spc: + return RoutePaths.SPC_DRIFT; + case DriftType.LLM: + return RoutePaths.LLM_DRIFT; + default: + throw new Error(`Unsupported drift type: ${driftType}`); + } + })(); + + // Make the request and store result in driftMap + const response = await createOpsmlClient(fetch).get(route, request); + const data = await response.json(); + driftMap[driftType as DriftType] = data; + } + ); + + // Wait for all requests to complete + await Promise.all(requests); + + return driftMap; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/card/readme/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/card/readme/utils.ts new file mode 100644 index 000000000..7616b7e80 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/card/readme/utils.ts @@ -0,0 +1,57 @@ +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; +import { RoutePaths } from "$lib/components/api/routes"; +import { RegistryType } from "$lib/utils"; +import { type ReadMe, type UploadResponse } from "$lib/components/readme/util"; +import type { CardQueryArgs, ReadMeArgs } from "$lib/components/api/schema"; + +/** + * Get the README content for a card. + * @param name The name of the card. + * @param space The space the card is in. + * @param registry_type The type of registry the card is in. + * @param fetch The fetch function to use. + * @returns The README content for the card. + */ +export async function getCardReadMe( + name: string, + space: string, + registry_type: RegistryType, + fetch: typeof globalThis.fetch +): Promise { + const params: CardQueryArgs = { + name: name, + space: space, + registry_type: registry_type, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.README, + params + ); + return await response.json(); +} + +/** + * Get the README content for a card. + * @param name The name of the card. + * @param space The space the card is in. + * @param registry_type The type of registry the card is in. + * @param fetch The fetch function to use. + * @returns The README content for the card. + */ +export async function createReadMe( + space: string, + name: string, + registry_type: RegistryType, + content: string, + fetch: typeof globalThis.fetch +): Promise { + let args: ReadMeArgs = { + space: space, + name: name, + registry_type: registry_type, + readme: content, + }; + + return (await createOpsmlClient(fetch).post(RoutePaths.README, args)).json(); +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/card/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/card/utils.ts new file mode 100644 index 000000000..0ac549681 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/card/utils.ts @@ -0,0 +1,197 @@ +import { RoutePaths } from "$lib/components/api/routes"; +import { RegistryType } from "$lib/utils"; +import type { + QueryPageResponse, + CardSpaceResponse, + RegistryStatsResponse, + RegistryPageReturn, + RegistryStatsRequest, + VersionPageResponse, + VersionPageRequest, + QueryPageRequest, + CardTagsResponse, +} from "$lib/components/card/types"; +import type { CardQueryArgs } from "$lib/components/api/schema"; +import { type Card } from "$lib/components/home/types"; +import { createOpsmlClient } from "../api/opsmlClient"; + +export async function getSpaces( + fetch: typeof globalThis.fetch, + registry_type: RegistryType +): Promise { + let params = { registry_type: registry_type }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.LIST_CARD_SPACES, + params + ); + return await response.json(); +} + +export async function getTags( + fetch: typeof globalThis.fetch, + registry_type: RegistryType +): Promise { + let params = { registry_type: registry_type }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.LIST_CARD_TAGS, + params + ); + return await response.json(); +} + +export async function getRegistryStats( + fetch: typeof globalThis.fetch, + registry_type: RegistryType, + searchTerm?: string, + spaces?: string[], + tags?: string[] +): Promise { + let request: RegistryStatsRequest = { + registry_type: registry_type, + search_term: searchTerm, + spaces: spaces, + tags: tags, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.GET_STATS, + request + ); + return await response.json(); +} + +export async function getRegistryPage( + fetch: typeof globalThis.fetch, + registry_type: RegistryType, + sort_by?: string, + spaces?: string[], + searchTerm?: string, + tags?: string[], + page?: number +): Promise { + let request: QueryPageRequest = { + registry_type: registry_type, + sort_by: sort_by, + spaces: spaces, + search_term: searchTerm, + tags: tags, + page: page, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.GET_REGISTRY_PAGE, + request + ); + return await response.json(); +} + +export async function setupRegistryPage( + registry_type: RegistryType, + space: undefined | string = undefined, + name: string | undefined = undefined, + fetch: typeof globalThis.fetch +): Promise { + const spaces = space ? [space] : undefined; + const [registry_spaces, tags, registryStats, registryPage] = + await Promise.all([ + getSpaces(fetch, registry_type), + getTags(fetch, registry_type), + getRegistryStats(fetch, registry_type, name, spaces), + getRegistryPage(fetch, registry_type, undefined, spaces, name), + ]); + + return { + spaces: registry_spaces.spaces, + tags: tags.tags, + registry_type: registry_type, + registryStats: registryStats, + registryPage: registryPage, + }; +} + +export async function getCardMetadata( + space: string | undefined, + name: string | undefined, + version: string | undefined, + uid: string | undefined, + registry_type: RegistryType, + fetch: typeof globalThis.fetch +): Promise { + const params: CardQueryArgs = { + name: name, + space: space, + version: version, + uid: uid, + registry_type: registry_type, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.METADATA, + params + ); + return await response.json(); +} + +export async function getVersionPage( + fetch: typeof globalThis.fetch, + registry_type: RegistryType, + space?: string, + name?: string, + page?: number +): Promise { + const params: VersionPageRequest = { + registry_type: registry_type, + space: space, + name: name, + page: page, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.GET_VERSION_PAGE, + params + ); + return await response.json(); +} + +export async function listRecentSpaceCards( + registry_type: RegistryType, + space: string, + fetch: typeof globalThis.fetch +): Promise { + const params: CardQueryArgs = { + space: space, + registry_type: registry_type, + sort_by_timestamp: true, + limit: 10, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.LIST_CARDS, + params + ); + const data = (await response.json()) as Card[]; + + return data; +} + +export async function getCardfromUid( + registry_type: RegistryType, + uid: string, + fetch: typeof globalThis.fetch +): Promise { + const params: CardQueryArgs = { + uid: uid, + registry_type: registry_type, + sort_by_timestamp: true, + limit: 10, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.LIST_CARDS, + params + ); + const data = (await response.json()) as Card[]; + return data; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/data/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/data/utils.ts new file mode 100644 index 000000000..8813b09d8 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/data/utils.ts @@ -0,0 +1,30 @@ +import { type DataCard } from "$lib/components/card/card_interfaces/datacard"; +import { type DataProfile } from "$lib/components/card/data/types"; +import { getRegistryTableName, RegistryType } from "$lib/utils"; +import { getRawFile } from "../card/files/utils"; +import { loadDataProfile } from "$lib/components/card/data/utils"; + +/** Helper function to get the data profile for a card + * @param card The DataCard to get the profile for + * @returns The DataProfile object + */ +export async function getDataProfile( + fetch: typeof globalThis.fetch, + card: DataCard +): Promise { + let dataProfileUri = card.metadata.interface_metadata.save_metadata + .data_profile_uri as string; + + let profilePath = `${getRegistryTableName(RegistryType.Data)}/${card.space}/${ + card.name + }/v${card.version}/${dataProfileUri}`; + + let rawFile = await getRawFile( + fetch, + profilePath, + card.uid, + RegistryType.Data + ); + + return loadDataProfile(rawFile.content); +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/experiment/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/experiment/utils.ts new file mode 100644 index 000000000..43199d7e5 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/experiment/utils.ts @@ -0,0 +1,164 @@ +import { createOpsmlClient } from "../api/opsmlClient"; +import { RoutePaths } from "$lib/components/api/routes"; +import type { + Metric, + Parameter, +} from "$lib/components/card/card_interfaces/experimentcard"; +import { type CardQueryArgs } from "$lib/components/api/schema"; +import type { Card } from "$lib/components/home/types"; +import { RegistryType, getRegistryTableName } from "$lib/utils"; +import type { + GetMetricNamesRequest, + GetMetricRequest, + GetParameterRequest, + GroupedMetrics, + Experiment, + UiMetricRequest, + GetHardwareMetricRequest, + HardwareMetrics, + UiHardwareMetrics, +} from "$lib/components/card/experiment/types"; +import { CommonPaths } from "$lib/components/card/experiment/types"; +import type { RawFile, RawFileRequest } from "$lib/components/files/types"; +import { extractAllHardwareMetrics } from "$lib/components/card/experiment/util"; + +// Get the metric names for a given experiment +export async function getCardMetricNames( + fetch: typeof globalThis.fetch, + uid: string +): Promise { + const request: GetMetricNamesRequest = { + experiment_uid: uid, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.EXPERIMENT_METRIC_NAMES, + request + ); + return (await response.json()) as string[]; +} + +export async function getCardParameters( + fetch: typeof globalThis.fetch, + uid: string +): Promise { + const request: GetParameterRequest = { + experiment_uid: uid, + names: [], + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.EXPERIMENT_PARAMETERS, + request + ); + return (await response.json()) as Parameter[]; +} + +// Get the metrics for a given experiment +export async function getCardMetrics( + fetch: typeof globalThis.fetch, + uid: string, + names: string[] +): Promise { + const request: GetMetricRequest = { + experiment_uid: uid, + names: names, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.EXPERIMENT_METRICS, + request + ); + return (await response.json()) as Metric[]; +} + +export async function getRecentExperiments( + fetch: typeof globalThis.fetch, + space: string, + name: string, + currentVersion: string +): Promise { + const params: CardQueryArgs = { + registry_type: RegistryType.Experiment, + name, + space, + limit: 50, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.LIST_CARDS, + params + ); + const cards = (await response.json()) as Card[]; + + return cards + .filter((card) => { + const version = card.data.version?.trim().toLowerCase(); + const current = currentVersion.trim().toLowerCase(); + return version && version !== current; + }) + .map((card) => ({ + uid: card.data.uid, + version: card.data.version, + })); +} + +export async function getGroupedMetrics( + fetch: typeof globalThis.fetch, + experiments: Experiment[], + selectedMetrics: string[] +): Promise { + let uiMetricRequest: UiMetricRequest = { + experiments: experiments, + metric_names: selectedMetrics, + }; + + // Process current card metrics + const response = await createOpsmlClient(fetch).post( + RoutePaths.EXPERIMENT_GROUPED_METRICS, + uiMetricRequest + ); + + return (await response.json()) as GroupedMetrics; +} + +export async function getHardwareMetrics( + fetch: typeof globalThis.fetch, + uid: string +): Promise { + const request: GetHardwareMetricRequest = { + experiment_uid: uid, + }; + + const response = await createOpsmlClient(fetch).get( + RoutePaths.HARDWARE_METRICS, + request + ); + let metrics = (await response.json()) as HardwareMetrics[]; + + return extractAllHardwareMetrics(metrics); +} + +export async function getExperimentFigures( + fetch: typeof globalThis.fetch, + uid: string, + space: string, + name: string, + version: string +): Promise { + const tableName = getRegistryTableName(RegistryType.Experiment); + const path = `${tableName}/${space}/${name}/v${version}/${CommonPaths.Artifacts}/${CommonPaths.Figures}`; + + const request: RawFileRequest = { + uid, + path, + registry_type: RegistryType.Experiment, + }; + + const response = await createOpsmlClient(fetch).post( + RoutePaths.FILE_CONTENT_BATCH, + request + ); + + return (await response.json()) as RawFile[]; +} diff --git a/crates/opsml_server/opsml_ui/src/lib/server/logger.ts b/crates/opsml_server/opsml_ui/src/lib/server/logger.ts new file mode 100644 index 000000000..938330424 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/logger.ts @@ -0,0 +1,4 @@ +import pino from "pino"; + +const logLevel = process.env.LOG_LEVEL || "info"; +export const logger = pino({ level: logLevel }); diff --git a/crates/opsml_server/opsml_ui/src/lib/server/settings.ts b/crates/opsml_server/opsml_ui/src/lib/server/settings.ts new file mode 100644 index 000000000..15848ad2a --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/lib/server/settings.ts @@ -0,0 +1,33 @@ +import { RoutePaths } from "$lib/components/api/routes"; +import type { UiSettings } from "$lib/components/settings/types"; +import { logger } from "$lib/server/logger"; +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; + +/** + * Fetch UI settings from the server (this is server-side only). + * @returns UI settings from the server + */ +export async function getUISettings( + fetch: typeof globalThis.fetch +): Promise { + try { + logger.debug("Fetching UI settings..."); + const opsmlClient = createOpsmlClient(fetch); + const response = await opsmlClient.get(RoutePaths.SETTINGS); + + if (!response.ok) { + throw new Error(`Failed to fetch settings: ${response.status}`); + } + + const data = (await response.json()) as UiSettings; + logger.debug(`Settings fetched successfully: ${JSON.stringify(data)}`); + return data; + } catch (error) { + logger.error(`Failed to fetch UI settings: ${error}`); + + return { + scouter_enabled: false, + sso_enabled: false, + }; + } +} diff --git a/crates/opsml_server/opsml_ui/src/lib/components/space/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/space/utils.ts similarity index 53% rename from crates/opsml_server/opsml_ui/src/lib/components/space/utils.ts rename to crates/opsml_server/opsml_ui/src/lib/server/space/utils.ts index 7eff73771..adbfe0583 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/space/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/server/space/utils.ts @@ -1,53 +1,55 @@ -import { opsmlClient } from "$lib/components/api/client.svelte"; import { RoutePaths } from "$lib/components/api/routes"; import type { CreateSpaceResponse, SpaceStatsResponse, SpaceRecordResponse, SpaceRecord, -} from "./types"; -import { userStore } from "../user/user.svelte"; -import { listRecentSpaceCards } from "../card/utils"; -import { RegistryType } from "$lib/utils"; +} from "$lib/components/space/types"; +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; export async function createSpace( + fetch: typeof globalThis.fetch, space: string, description: string ): Promise { let params = { space: space, description: description }; - const response = await opsmlClient.post( + const response = await createOpsmlClient(fetch).post( RoutePaths.CREATE_SPACE, - params, - userStore.jwt_token + params ); return await response.json(); } -export async function deleteSpace(space: string): Promise { +export async function deleteSpace( + fetch: typeof globalThis.fetch, + space: string +): Promise { let params = { space: space }; - const response = await opsmlClient.delete( + const response = await createOpsmlClient(fetch).delete( RoutePaths.DELETE_SPACE, - params, - userStore.jwt_token + params ); return await response.json(); } -export async function getAllSpaceStats(): Promise { - const response = await opsmlClient.get( +export async function getAllSpaceStats( + fetch: typeof globalThis.fetch +): Promise { + const response = await createOpsmlClient(fetch).get( RoutePaths.ALL_SPACES, - undefined, - userStore.jwt_token + undefined ); return await response.json(); } -export async function getSpace(space: string): Promise { +export async function getSpace( + fetch: typeof globalThis.fetch, + space: string +): Promise { let params = { space: space }; - const response = await opsmlClient.get( + const response = await createOpsmlClient(fetch).get( RoutePaths.SPACES, - params, - userStore.jwt_token + params ); let recordResponse = (await response.json()) as SpaceRecordResponse; // if no items found, return empty record diff --git a/crates/opsml_server/opsml_ui/src/lib/components/user/utils.ts b/crates/opsml_server/opsml_ui/src/lib/server/user/utils.ts similarity index 65% rename from crates/opsml_server/opsml_ui/src/lib/components/user/utils.ts rename to crates/opsml_server/opsml_ui/src/lib/server/user/utils.ts index 4485c8724..4a5b357f5 100644 --- a/crates/opsml_server/opsml_ui/src/lib/components/user/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/server/user/utils.ts @@ -1,4 +1,3 @@ -import { opsmlClient } from "$lib/components/api/client.svelte"; import { RoutePaths } from "$lib/components/api/routes"; import type { CreateUserRequest, @@ -6,46 +5,46 @@ import type { RecoveryResetRequest, ResetPasswordResponse, UserResponse, - LogOutResponse, UpdateUserRequest, SsoAuthUrl, LoginResponse, } from "$lib/components/user/types"; -import { userStore } from "./user.svelte"; +import { createOpsmlClient } from "../api/opsmlClient"; // Helper function for registering a user via the api client // It is assumed that all arguments have been validated prior to calling this function export async function registerUser( username: string, password: string, - email: string + email: string, + fetch: typeof globalThis.fetch ): Promise { const request: CreateUserRequest = { username: username, password: password, email: email, }; - const response = await opsmlClient.post( - RoutePaths.REGISTER, - request, - userStore.jwt_token - ); + let opsmlClient = createOpsmlClient(fetch); + const response = await opsmlClient.post(RoutePaths.REGISTER, request); return (await response.json()) as CreateUserUiResponse; } // Helper for getting user information for user section -export async function getUser(username: string): Promise { +export async function getUser( + username: string, + fetch: typeof globalThis.fetch +): Promise { + let opsmlClient = createOpsmlClient(fetch); let path = `${RoutePaths.USER}/${username}`; - - const response = await opsmlClient.get(path, undefined, userStore.jwt_token); - + const response = await opsmlClient.get(path, undefined); return (await response.json()) as UserResponse; } export async function resetUserPassword( username: string, recovery_code: string, - newPassword: string + newPassword: string, + fetch: typeof globalThis.fetch ): Promise { const request: RecoveryResetRequest = { username: username, @@ -53,23 +52,11 @@ export async function resetUserPassword( new_password: newPassword, }; - const response = await opsmlClient.post( - RoutePaths.RESET_PASSWORD, - request, - userStore.jwt_token - ); - + let opsmlClient = createOpsmlClient(fetch); + const response = await opsmlClient.post(RoutePaths.RESET_PASSWORD, request); return (await response.json()) as ResetPasswordResponse; } -export async function logout(): Promise { - let path = `${RoutePaths.LOGOUT}`; - - const response = await opsmlClient.get(path, undefined, userStore.jwt_token); - - return (await response.json()) as LogOutResponse; -} - interface UpdateUserOptions { permissions?: string[]; group_permissions?: string[]; @@ -77,7 +64,9 @@ interface UpdateUserOptions { } export async function updateUser( - options: UpdateUserOptions + fetch: typeof globalThis.fetch, + options: UpdateUserOptions, + username: string ): Promise { const request: UpdateUserRequest = { permissions: options.permissions, @@ -85,13 +74,17 @@ export async function updateUser( favorite_spaces: options.favorite_spaces, }; - let path = `${RoutePaths.USER}/${userStore.username}`; + let path = `${RoutePaths.USER}/${username}`; - const response = await opsmlClient.put(path, request, userStore.jwt_token); + let opsmlClient = createOpsmlClient(fetch); + const response = await opsmlClient.put(path, request); return (await response.json()) as UserResponse; } -export async function getSsoAuthURL(): Promise { +export async function getSsoAuthURL( + fetch: typeof globalThis.fetch +): Promise { + let opsmlClient = createOpsmlClient(fetch); const path = `${RoutePaths.SSO_AUTH}`; const response = await opsmlClient.get(path); @@ -101,9 +94,11 @@ export async function getSsoAuthURL(): Promise { export async function exchangeSsoCallbackCode( code: string, - codeVerifier: string + codeVerifier: string, + fetch: typeof globalThis.fetch ): Promise { const path = `${RoutePaths.SSO_CALLBACK}`; + let opsmlClient = createOpsmlClient(fetch); const response = await opsmlClient.get(path, { code, code_verifier: codeVerifier, diff --git a/crates/opsml_server/opsml_ui/src/lib/utils.ts b/crates/opsml_server/opsml_ui/src/lib/utils.ts index a392de751..bb136ee29 100644 --- a/crates/opsml_server/opsml_ui/src/lib/utils.ts +++ b/crates/opsml_server/opsml_ui/src/lib/utils.ts @@ -5,6 +5,7 @@ export enum RegistryType { Prompt = "prompt", Service = "service", Mcp = "mcp", + Agent = "agent", } export function getRegistryTypeLowerCase(type: RegistryType): string { @@ -32,6 +33,8 @@ export function getRegistryPath(type: RegistryType): string { return "service"; case RegistryType.Mcp: return "genai/mcp"; + case RegistryType.Agent: + return "genai/agent"; default: return ""; } @@ -49,6 +52,8 @@ export function getRegistryFromString(type: string): RegistryType | undefined { return RegistryType.Prompt; case "service": return RegistryType.Service; + case "mcp": + return RegistryType.Mcp; default: return undefined; } diff --git a/crates/opsml_server/opsml_ui/src/routes/+layout.server.ts b/crates/opsml_server/opsml_ui/src/routes/+layout.server.ts new file mode 100644 index 000000000..0e9703d71 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/+layout.server.ts @@ -0,0 +1,28 @@ +import { getUISettings } from "$lib/server/settings"; +import { logger } from "$lib/server/logger"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ fetch }) => { + try { + logger.debug("Loading layout server data..."); + + const settings = await getUISettings(fetch); + + return { + settings: { + scouter_enabled: settings.scouter_enabled, + sso_enabled: settings.sso_enabled, + }, + }; + } catch (error) { + logger.error(`Layout server load error: ${error}`); + + // Return default settings if API is unavailable + return { + settings: { + scouter_enabled: false, + sso_enabled: false, + }, + }; + } +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/+layout.svelte index 42009aca4..719e2814c 100644 --- a/crates/opsml_server/opsml_ui/src/routes/+layout.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/+layout.svelte @@ -5,12 +5,16 @@ import Navbar from "$lib/components/nav/Navbar.svelte"; import { onMount } from 'svelte'; import { ToastProvider } from '@skeletonlabs/skeleton-svelte'; + import { uiSettingsStore } from "$lib/components/settings/settings.svelte"; - let { children } = $props(); + let { data, children} = $props(); - let show = $state(false); + // initialize settings store with data from server + uiSettingsStore.initialize(data.settings); + let show = $state(false); + onMount(() => { setTimeout(() => { show = true; @@ -51,5 +55,6 @@ linear-gradient(to right, #CECBDB 1px, transparent 1px), linear-gradient(to bottom, #CECBDB 1px, transparent 1px); background-size: 60px 60px; + background-attachment: fixed; } \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/+layout.ts deleted file mode 100644 index 79e4205df..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/+layout.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const prerender = true; -export const ssr = false; - -import { uiSettingsStore } from "$lib/components/settings/settings.svelte"; - -// @ts-ignore -import type { LayoutServerLoad } from "./$types"; - -export const load: LayoutServerLoad = async () => { - await uiSettingsStore.getSettings(); - return {}; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/+page.ts b/crates/opsml_server/opsml_ui/src/routes/+page.ts new file mode 100644 index 000000000..717d142ae --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/+page.ts @@ -0,0 +1,9 @@ +import { redirect } from "@sveltejs/kit"; + +/** + * Redirects root URL "/" to the main application page "/opsml/home". + * Ensures users always land on the main dashboard. + */ +export function load() { + throw redirect(302, "/opsml/home"); +} diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/data/profile/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/data/profile/+server.ts new file mode 100644 index 000000000..58be93062 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/data/profile/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getDataProfile } from "$lib/server/data/utils"; + +/** Get a page of LLM monitoring records + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { card } = await request.json(); + const response = await getDataProfile(fetch, card); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/hardware/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/hardware/+server.ts new file mode 100644 index 000000000..684d8f2bc --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/hardware/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getHardwareMetrics } from "$lib/server/experiment/utils"; + +/** Get the hardware metrics for a set of experiments + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { uid } = await request.json(); + const response = await getHardwareMetrics(fetch, uid); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/grouped/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/grouped/+server.ts new file mode 100644 index 000000000..4fed2cc92 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/grouped/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getGroupedMetrics } from "$lib/server/experiment/utils"; + +/** Get the grouped metrics for a set of experiments + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { experiments, selectedMetrics } = await request.json(); + const response = await getGroupedMetrics(fetch, experiments, selectedMetrics); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/names/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/names/+server.ts new file mode 100644 index 000000000..84d210382 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/metrics/names/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getCardMetricNames } from "$lib/server/experiment/utils"; + +/** Get the metric names for a given experiment + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { uid } = await request.json(); + const response = await getCardMetricNames(fetch, uid); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/recent/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/recent/+server.ts new file mode 100644 index 000000000..c93f72f49 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/experiment/recent/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getRecentExperiments } from "$lib/server/experiment/utils"; + +/** Get recent experiments for a space and name + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { space, name, version } = await request.json(); + const response = await getRecentExperiments(fetch, space, name, version); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/metadata/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/metadata/+server.ts new file mode 100644 index 000000000..d505ec568 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/metadata/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from "./$types"; +import { json } from "@sveltejs/kit"; +import { getCardMetadata } from "$lib/server/card/utils"; + +/** Get Card Metadata + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { space, name, version, uid, registry_type } = await request.json(); + + const response = await getCardMetadata( + space, + name, + version, + uid, + registry_type, + fetch + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/+server.ts new file mode 100644 index 000000000..de80bde32 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/+server.ts @@ -0,0 +1,17 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getDriftAlerts } from "$lib/server/card/monitoring/utils"; + +/** Get a page of recent drift alerts + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { space, name, version, timeInterval, active } = await request.json(); + const response = await getDriftAlerts( + fetch, + space, + name, + version, + timeInterval, + active + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/acknowledge/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/acknowledge/+server.ts new file mode 100644 index 000000000..bfe0bfb0d --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/alerts/acknowledge/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { acknowledgeAlert } from "$lib/server/card/monitoring/utils"; + +/** Get a page of recent drift alerts + */ +export const PUT: RequestHandler = async ({ request, fetch }) => { + const { id, space } = await request.json(); + const response = await acknowledgeAlert(fetch, id, space); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/llm/record/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/llm/record/+server.ts new file mode 100644 index 000000000..f4301d96f --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/llm/record/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getLLMRecordPage } from "$lib/server/card/monitoring/utils"; + +/** Get a page of LLM monitoring records + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { service_info, status, cursor } = await request.json(); + const response = await getLLMRecordPage(fetch, service_info, status, cursor); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/metrics/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/metrics/+server.ts new file mode 100644 index 000000000..af53b7c27 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/metrics/+server.ts @@ -0,0 +1,15 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getLatestMetrics } from "$lib/server/card/monitoring/utils"; + +/** Get a page of latest metrics for drift profiles + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { profiles, time_interval, max_data_points } = await request.json(); + const response = await getLatestMetrics( + fetch, + profiles, + time_interval, + max_data_points + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profile/update/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profile/update/+server.ts new file mode 100644 index 000000000..8cca1ad34 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profile/update/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { updateDriftProfile } from "$lib/server/card/monitoring/utils"; + +/** Get a page of recent drift alerts + */ +export const PUT: RequestHandler = async ({ request, fetch }) => { + const { update_request } = await request.json(); + const response = await updateDriftProfile(fetch, update_request); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profiles/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profiles/+server.ts new file mode 100644 index 000000000..4a28a60e6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/monitoring/profiles/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { getDriftProfiles } from "$lib/server/card/monitoring/utils"; + +/** Get a page of latest metrics for drift profiles + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { uid, driftMap, registryType } = await request.json(); + const response = await getDriftProfiles(fetch, uid, driftMap, registryType); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/readme/create/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/readme/create/+server.ts new file mode 100644 index 000000000..fcb90eb20 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/readme/create/+server.ts @@ -0,0 +1,17 @@ +import type { RequestHandler } from "./$types"; +import { json } from "@sveltejs/kit"; +import { createReadMe } from "$lib/server/card/readme/utils"; + +/** Get Card Metadata + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { space, name, registry_type, content } = await request.json(); + const response = await createReadMe( + space, + name, + registry_type, + content, + fetch + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/registry/page/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/page/+server.ts new file mode 100644 index 000000000..3b46a2c21 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/page/+server.ts @@ -0,0 +1,20 @@ +import type { RequestHandler } from "./$types"; +import { getRegistryPage } from "$lib/server/card/utils"; +import { json } from "@sveltejs/kit"; + +/** Get Registry Page Data + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { registry_type, sort_by, spaces, searchTerm, tags, page } = + await request.json(); + const response = await getRegistryPage( + fetch, + registry_type, + sort_by, + spaces, + searchTerm, + tags, + page + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/registry/stats/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/stats/+server.ts new file mode 100644 index 000000000..c2305ac34 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/stats/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from "./$types"; +import { getRegistryStats } from "$lib/server/card/utils"; +import { json } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger"; + +/** Get Registry stats data + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { registry_type, searchTerm, spaces, tags } = await request.json(); + const response = await getRegistryStats( + fetch, + registry_type, + searchTerm, + spaces, + tags + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/registry/version/page/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/version/page/+server.ts new file mode 100644 index 000000000..433294fa9 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/registry/version/page/+server.ts @@ -0,0 +1,17 @@ +import type { RequestHandler } from "../$types"; +import { getVersionPage } from "$lib/server/card/utils"; +import { json } from "@sveltejs/kit"; + +/** Get a page of versions for a registry item + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { registry_type, space, name, page } = await request.json(); + const response = await getVersionPage( + fetch, + registry_type, + space, + name, + page + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/card/uid/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/card/uid/+server.ts new file mode 100644 index 000000000..90cd34484 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/card/uid/+server.ts @@ -0,0 +1,19 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { getCardfromUid } from "$lib/server/card/utils"; +import { RegistryType } from "$lib/utils"; + +/** + * Server route to fetch card information by UID. + * Client should provide the UID in the request. + */ +export const GET: RequestHandler = async ({ fetch, url }) => { + let registry_type: RegistryType = url.searchParams.get( + "registry_type" + ) as RegistryType; + let uid = url.searchParams.get("uid") || ""; + let response = await getCardfromUid(registry_type, uid, fetch); + + // Return card info to client for further handling + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/health/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/health/+server.ts new file mode 100644 index 000000000..2cb18b091 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/health/+server.ts @@ -0,0 +1,7 @@ +// Health check endpoint + +import type { RequestHandler } from "@sveltejs/kit"; + +export const GET: RequestHandler = async () => { + return new Response("OK", { status: 200 }); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/space/create/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/space/create/+server.ts new file mode 100644 index 000000000..516f40e6a --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/space/create/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { createSpace } from "$lib/server/space/utils"; + +/** Get a page of LLM monitoring records + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { space, description } = await request.json(); + const response = await createSpace(fetch, space, description); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/info/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/info/+server.ts new file mode 100644 index 000000000..dfee6448e --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/info/+server.ts @@ -0,0 +1,29 @@ +import type { RequestHandler } from "./$types"; +import { json } from "@sveltejs/kit"; +import { getUser } from "$lib/server/user/utils"; +import { logger } from "$lib/server/logger"; + +/** + * Top-level GET to get user info if jwt and username cookies are set. + * Only calls getUser if both are present; otherwise returns success: false. + */ +export const GET: RequestHandler = async ({ cookies, fetch }) => { + const jwt_token = cookies.get("jwt_token"); + const username = cookies.get("username"); + let response = { success: false, user: null }; + + if (jwt_token && username) { + try { + const user = await getUser(username, fetch); + return json({ success: true, user }); + } catch (error) { + logger.error(`Error fetching user info: ${error}`); + return json( + { success: false, user: null, error: "User not found" }, + { status: 404 } + ); + } + } + + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/login/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/login/+server.ts new file mode 100644 index 000000000..2e1e0b9c3 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/login/+server.ts @@ -0,0 +1,38 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { + loginUser, + setTokenInCookies, + setUsernameInCookies, +} from "$lib/server/auth/validateToken"; +import { logger } from "$lib/server/logger"; + +/** + * Handles login requests and sets JWT cookie. + * Only authentication and cookie logic here; UI navigation is handled client-side. + */ +export const POST: RequestHandler = async ({ request, cookies, fetch }) => { + const { username, password } = await request.json(); + + logger.debug(`Login attempt for user: ${username}`); + const loginResponse = await loginUser(fetch, username, password); + + if (loginResponse.authenticated) { + // Return success response with JWT token + setTokenInCookies(cookies, loginResponse.jwt_token); + setUsernameInCookies(cookies, username); + + // mask the token in the response after setting the cookie + loginResponse.jwt_token = "*****"; + + return json({ success: true, response: loginResponse }); + } else { + return json( + { + success: false, + error: loginResponse.message ?? "Invalid username or password", + }, + { status: 401 } + ); + } +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/logout/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/logout/+server.ts new file mode 100644 index 000000000..8793019f4 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/logout/+server.ts @@ -0,0 +1,35 @@ +import type { RequestHandler } from "./$types"; +import { setTokenInCookies } from "$lib/server/auth/validateToken"; +import type { LogOutResponse } from "$lib/components/user/types"; +import { RoutePaths } from "$lib/components/api/routes"; +import { json } from "@sveltejs/kit"; +import { createOpsmlClient } from "$lib/server/api/opsmlClient"; + +/** + * Helper function for logging out a user via the api client + * @returns + */ +async function logout( + fetch: typeof globalThis.fetch, + jwt_token?: string +): Promise { + let opsmlClient = createOpsmlClient(fetch); + let path = `${RoutePaths.LOGOUT}`; + const response = await opsmlClient.get(path, undefined); + return (await response.json()) as LogOutResponse; +} + +/** + * Handles login requests and sets JWT cookie. + * Only authentication and cookie logic here; UI navigation is handled client-side. + */ +export const GET: RequestHandler = async ({ cookies, fetch }) => { + let loggedOut = await logout(fetch); + + if (loggedOut) { + // update the user store + setTokenInCookies(cookies, ""); // clear the cookie + } + + return json(loggedOut); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/register/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/register/+server.ts new file mode 100644 index 000000000..72e0704e2 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/register/+server.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from "./$types"; +import { registerUser } from "$lib/server/user/utils"; +import { json } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger"; + +/** + * Handles login requests and sets JWT cookie. + * Only authentication and cookie logic here; UI navigation is handled client-side. + */ +export const POST: RequestHandler = async ({ request, cookies, fetch }) => { + const { username, password, email } = await request.json(); + const response = await registerUser(username, password, email, fetch); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/reset/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/reset/+server.ts new file mode 100644 index 000000000..3249633ae --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/reset/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from "./$types"; +import { resetUserPassword } from "$lib/server/user/utils"; +import { json } from "@sveltejs/kit"; + +/** + * Handles login requests and sets JWT cookie. + * Only authentication and cookie logic here; UI navigation is handled client-side. + */ +export const POST: RequestHandler = async ({ request, fetch }) => { + const { username, recovery_code, new_password } = await request.json(); + const response = await resetUserPassword( + username, + recovery_code, + new_password, + fetch + ); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/sso/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/sso/+server.ts new file mode 100644 index 000000000..5353ac9ef --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/sso/+server.ts @@ -0,0 +1,19 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { getSsoAuthURL } from "$lib/server/user/utils"; +import { logger } from "$lib/server/logger"; + +/** + * Server route to fetch SSO authentication URL and related state. + * Client should store state/code_verifier and perform redirect. + */ +export const GET: RequestHandler = async ({ fetch, url }) => { + // Fetch SSO auth URL and related info from backend + const ssoAuthUrl = await getSsoAuthURL(fetch); + + // Log the SSO URL fetch action + logger.debug(`Fetched SSO authentication URL. ${JSON.stringify(ssoAuthUrl)}`); + + // Return SSO info to client for further handling + return json(ssoAuthUrl); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/sso/callback/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/sso/callback/+server.ts new file mode 100644 index 000000000..e03cbc1d6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/sso/callback/+server.ts @@ -0,0 +1,23 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { exchangeSsoCallbackCode } from "$lib/server/user/utils"; +import { + setTokenInCookies, + setUsernameInCookies, +} from "$lib/server/auth/validateToken"; + +export const POST: RequestHandler = async ({ request, fetch, cookies }) => { + const { code, code_verifier } = await request.json(); + const loginResponse = await exchangeSsoCallbackCode( + code, + code_verifier, + fetch + ); + setTokenInCookies(cookies, loginResponse.jwt_token); + setUsernameInCookies(cookies, loginResponse.username); + + // mask the token in the response after setting the cookie + loginResponse.jwt_token = "*****"; + + return json(loginResponse); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/api/user/update/+server.ts b/crates/opsml_server/opsml_ui/src/routes/api/user/update/+server.ts new file mode 100644 index 000000000..12c2d39c4 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/api/user/update/+server.ts @@ -0,0 +1,10 @@ +import { type RequestHandler, json } from "@sveltejs/kit"; +import { updateUser } from "$lib/server/user/utils"; + +/** Update user details + */ +export const PUT: RequestHandler = async ({ request, fetch }) => { + const { options, username } = await request.json(); + const response = await updateUser(fetch, options, username); + return json(response); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/+layout.ts new file mode 100644 index 000000000..16e420a20 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/+layout.ts @@ -0,0 +1,20 @@ +import type { LayoutLoad } from "./$types"; +import { createInternalApiClient } from "$lib/api/internalClient"; +import { ServerPaths } from "$lib/components/api/routes"; +import type { UserResponse } from "$lib/components/user/types"; +import { userStore } from "$lib/components/user/user.svelte"; + +export const load: LayoutLoad = async ({ fetch }) => { + const resp = await createInternalApiClient(fetch).get(ServerPaths.USER); + const user = (await resp.json()) as { + success: boolean; + user: UserResponse | null; + }; + + if (user.success) { + let userResponse = user.user as UserResponse; + userStore.fromUserResponse(userResponse); + } + + return { user }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.server.ts new file mode 100644 index 000000000..25b1938d0 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.server.ts @@ -0,0 +1,9 @@ +import type { LayoutServerLoad } from "./$types"; +import { getRegistryFromString } from "$lib/utils"; + +export const load: LayoutServerLoad = async ({ params }) => { + let registryType = getRegistryFromString(params.registry); + return { + registryType, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.svelte similarity index 69% rename from crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.svelte index c934f0101..f92199147 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+layout.svelte @@ -1,10 +1,13 @@
+ {#key page.params.registry} {@render children()} + {/key}
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.server.ts new file mode 100644 index 000000000..ea4ee2b16 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.server.ts @@ -0,0 +1,34 @@ +import { setupRegistryPage } from "$lib/server/card/utils"; +import { getRegistryFromString, RegistryType } from "$lib/utils"; +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad, EntryGenerator } from "./$types"; + +export const entries: EntryGenerator = () => { + return [ + { registry: "model" }, + { registry: "data" }, + { registry: "experiment" }, + { registry: "service" }, + ]; +}; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + let { registryType } = await parent(); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } + + let registryPage = await setupRegistryPage( + registryType, + undefined, + undefined, + fetch + ); + + return { + page: registryPage, + selectedSpace: undefined, + selectedName: undefined, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.svelte new file mode 100644 index 000000000..ebbc454d9 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/+page.svelte @@ -0,0 +1,15 @@ + + + + + + diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/[uid]/+page.server.ts similarity index 50% rename from crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/[uid]/+page.ts rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/[uid]/+page.server.ts index 931486e36..80de0f0d4 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/[uid]/+page.ts +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/[uid]/+page.server.ts @@ -1,23 +1,27 @@ -export const ssr = false; -export const prerender = false; import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import { getCardMetadata } from "$lib/components/card/utils"; +import type { PageServerLoad } from "./$types"; import { getRegistryTypeLowerCase } from "$lib/utils"; import type { ServiceCard } from "$lib/components/card/card_interfaces/servicecard"; +import { getCardMetadata } from "$lib/server/card/utils"; +import type { BaseCard } from "$lib/components/home/types"; -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); +export const load: PageServerLoad = async ({ parent, params, fetch }) => { + let { registryType } = await parent(); + + console.log("uid param:", params.uid); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } let metadata = (await getCardMetadata( undefined, undefined, undefined, params.uid, - registryType - )) as ServiceCard; + registryType, + fetch + )) as BaseCard; redirect( 301, diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.server.ts new file mode 100644 index 000000000..2d5950bff --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.server.ts @@ -0,0 +1,20 @@ +import { setupRegistryPage } from "$lib/server/card/utils"; +import type { PageServerLoad } from "./$types"; +import { redirect } from "@sveltejs/kit"; + +export const load: PageServerLoad = async ({ parent, params, fetch }) => { + const space = params.space; + const name = undefined; // No name parameter in this route + let { registryType } = await parent(); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } + + let registryPage = await setupRegistryPage(registryType, space, name, fetch); + return { + page: registryPage, + selectedSpace: space, + selectedName: name, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.svelte similarity index 81% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.svelte index 512d359b7..b802292c5 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/+page.svelte @@ -1,7 +1,7 @@ + + + + {@render children()} + \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/card/+page.svelte new file mode 100644 index 000000000..a5877ef79 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/card/+page.svelte @@ -0,0 +1,30 @@ + + +{#if registryType === RegistryType.Data} + +{:else if registryType === RegistryType.Model} + +{:else if registryType === RegistryType.Experiment} + +{:else if registryType === RegistryType.Service} + +{:else} +
+
+

Unknown registry type: {registryType}

+
+
+{/if} + diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/figures/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/figures/+page.server.ts new file mode 100644 index 000000000..ba1b8ec1d --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/figures/+page.server.ts @@ -0,0 +1,17 @@ +import { getExperimentFigures } from "$lib/server/experiment/utils"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + const { metadata } = await parent(); + + // get metric names, parameters + let experimentFigures = await getExperimentFigures( + fetch, + metadata.uid, + metadata.space, + metadata.name, + metadata.version + ); + + return { figures: experimentFigures }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/figures/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/figures/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/figures/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/figures/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/+page.server.ts new file mode 100644 index 000000000..a89a7acea --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/+page.server.ts @@ -0,0 +1,14 @@ +import type { PageServerLoad } from "./$types"; +import { getFileTree } from "$lib/server/card/files/utils"; +import { getRegistryTableName } from "$lib/utils"; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + const { metadata, registryType } = await parent(); + + let tableName = getRegistryTableName(registryType); + let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; + + let fileTree = await getFileTree(fetch, basePath); + + return { fileTree, previousPath: basePath, isRoot: true }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.server.ts new file mode 100644 index 000000000..b530d7468 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.server.ts @@ -0,0 +1,6 @@ +import type { PageServerLoad } from "./$types"; +import { loadFileTree } from "$lib/components/files/fileTreeLoader"; + +export const load: PageServerLoad = async (args) => { + return await loadFileTree(args); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/[...file]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/[...file]/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts new file mode 100644 index 000000000..ae92372e6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts @@ -0,0 +1,13 @@ +import type { PageServerLoad } from "./$types"; +import { getRawFile } from "$lib/server/card/files/utils"; +import { RegistryType } from "$lib/utils"; + +export const load: PageServerLoad = async ({ parent, url, fetch }) => { + const { registryType, metadata } = await parent(); + const viewPath = (url as URL).searchParams.get("path") as string; + + let rawFile = await getRawFile(fetch, viewPath, metadata.uid, registryType); + let splitPath = viewPath.split("/"); + + return { rawFile, viewPath, splitPath }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/view/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/view/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/view/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/files/view/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/hardware/+page.svelte similarity index 91% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/hardware/+page.svelte index da021dbca..48b4b565b 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/hardware/+page.svelte @@ -14,10 +14,10 @@ -
+
-
+
-
+
-
+
-
+
{ + const { registryType, metadata } = await parent(); + + if (registryType !== RegistryType.Experiment) { + throw goto( + `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}` + ); + } + + // get metric names, parameters + let resp = await createInternalApiClient(fetch).post( + ServerPaths.EXPERIMENT_HARDWARE, + { + uid: metadata.uid, + } + ); + const hardwareMetrics = (await resp.json()) as UiHardwareMetrics; + + return { hardwareMetrics }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.svelte similarity index 51% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.svelte index 69578a7a7..e1c1b8006 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.svelte @@ -1,57 +1,42 @@ -
-
+
+
-
+
-
+
-
+
-
Search Metrics
+
Search Metrics
-
+
Plot Type:
-
-
- -
- +
-
- -
-
-
- -
Items
-
-
- - {#each filteredEntities as entity} - - {#if selectedMetrics.includes(entity)} - - {:else} - - {/if} - - {/each} -
-
- -
-
- -
Previous Versions
-
-

Select previous version to compare metrics

-
- {#each recentExperiments as experiment} - {#if selectedExperiments.includes(experiment)} - - {:else} - - {/if} - {/each} -
+
+
@@ -163,7 +120,7 @@ -
+
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.ts new file mode 100644 index 000000000..d30fb1b71 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/metrics/+page.ts @@ -0,0 +1,38 @@ +export const ssr = false; + +import { getRegistryPath, RegistryType } from "$lib/utils"; +import { goto } from "$app/navigation"; +import type { PageLoad } from "./$types"; +import { ServerPaths } from "$lib/components/api/routes"; +import { createInternalApiClient } from "$lib/api/internalClient"; +import type { Experiment } from "$lib/components/card/experiment/types"; + +export const load: PageLoad = async ({ parent, fetch }) => { + const { registryType, metadata } = await parent(); + + if (registryType !== RegistryType.Experiment) { + throw goto( + `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}` + ); + } + + const [metricNamesResp, recentExperimentsResp] = await Promise.all([ + createInternalApiClient(fetch).post(ServerPaths.EXPERIMENT_METRIC_NAMES, { + uid: metadata.uid, + }), + createInternalApiClient(fetch).post(ServerPaths.EXPERIMENT_RECENT, { + space: metadata.space, + name: metadata.name, + version: metadata.version, + }), + ]); + + const [metricNames, recentExperiments] = await Promise.all([ + metricNamesResp.json() as Promise, + recentExperimentsResp.json() as Promise, + ]); + + return { metadata, metricNames, recentExperiments }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte new file mode 100644 index 000000000..718aa1508 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts new file mode 100644 index 000000000..8bfa8b52c --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts @@ -0,0 +1,27 @@ +// Base data for monitoring page. +// This needs to be client-side because we need to calculate max data points from window size +export const ssr = false; + +import type { PageLoad } from "./$types"; +import { TimeInterval } from "$lib/components/card/monitoring/types"; +import { loadMonitoringDashboardData } from "$lib/components/card/monitoring/getMonitoringDashboardData"; +import { getRegistryPath, RegistryType } from "$lib/utils"; +import { goto } from "$app/navigation"; + +export const load: PageLoad = async ({ parent, fetch }) => { + const { registryType, metadata } = await parent(); + + // redirect to card layout if registryType is not 'model' + if (registryType !== RegistryType.Model) { + throw goto( + `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}` + ); + } + return loadMonitoringDashboardData(fetch, await parent(), { + initialTimeInterval: TimeInterval.SixHours, + loadLLMRecords: false, + loadAlerts: true, + }); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.svelte new file mode 100644 index 000000000..3e1b22f76 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.svelte @@ -0,0 +1,33 @@ + + + +{#if dataProfile} +
+ +
+{:else} +
+
+
+

Data Profile

+

+ No data profile is available for this card. +

+
+
+
+{/if} diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.ts new file mode 100644 index 000000000..9f11564cb --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/profile/+page.ts @@ -0,0 +1,34 @@ +export const ssr = false; + +import { redirect } from "@sveltejs/kit"; +import { getSortedFeatureNames } from "$lib/components/card/data/utils"; +import { getRegistryPath, RegistryType } from "$lib/utils"; +import type { PageLoad } from "./$types"; +import { getDataProfile } from "$lib/components/card/data/getDataProfile"; + +export const load: PageLoad = async ({ parent, fetch }) => { + const { registryType, metadata } = await parent(); + + if (registryType !== RegistryType.Data) { + throw redirect( + 302, + `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}` + ); + } + + let dataProfile = metadata.metadata.interface_metadata.save_metadata + ?.data_profile_uri + ? await getDataProfile(fetch, metadata) + : undefined; + + // get sorted feature names from dataProfile.features + let featureNames: string[] = []; + + if (dataProfile) { + featureNames = getSortedFeatureNames(dataProfile); + } + + return { dataProfile, featureNames }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/readme/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/readme/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/readme/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/readme/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/readme/+page.ts new file mode 100644 index 000000000..02003744e --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/readme/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from "./$types"; +import { generateReadmeContent } from "$lib/components/readme/readmeGenerator"; + +export const load: PageLoad = async ({ parent }) => { + const { metadata, registryType, readme } = await parent(); + + const content = generateReadmeContent(registryType, metadata, readme); + + return { + metadata, + content, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts new file mode 100644 index 000000000..f295f088b --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts @@ -0,0 +1,13 @@ +import { getRegistryStats, getVersionPage } from "$lib/server/card/utils"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + const { metadata, registryType } = await parent(); + + let [versionPage, versionStats] = await Promise.all([ + getVersionPage(fetch, registryType, metadata.space, metadata.name), + getRegistryStats(fetch, registryType, metadata.name, [metadata.space]), + ]); + + return { versionPage, versionStats }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/versions/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/versions/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/[registry]/card/[space]/[name]/[version]/versions/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.svelte deleted file mode 100644 index c934f0101..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -
- - {@render children()} - -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.ts deleted file mode 100644 index 15836fde7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Data, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/+page.ts deleted file mode 100644 index 6fc388eb0..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/+page.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import { RegistryType } from "$lib/utils"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/[uid]/+page.ts deleted file mode 100644 index dedee41f6..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/[uid]/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; -export const prerender = false; -import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import type { DataCard } from "$lib/components/card/card_interfaces/datacard"; -import { getCardMetadata } from "$lib/components/card/utils"; -import { getRegistryPath } from "$lib/utils"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let metadata = (await getCardMetadata( - undefined, - undefined, - undefined, - params.uid, - registryType - )) as DataCard; - - redirect( - 301, - `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ - metadata.name - }/${metadata.version}/card` - ); -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/+page.ts deleted file mode 100644 index 720f32489..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import { RegistryType } from "$lib/utils"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.ts deleted file mode 100644 index 684388528..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import { RegistryType } from "$lib/utils"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index 76a930728..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
- - - - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index ea6846664..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getRegistryTypeLowerCase, RegistryType } from "$lib/utils"; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import type { DataCard } from "$lib/components/card/card_interfaces/datacard"; -import { getCardReadMe } from "$lib/components/readme/util"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as DataCard; - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index 38728fa66..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - return { metadata, registryType, readme }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.ts deleted file mode 100644 index 0c1696d53..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - const { metadata, registryType } = await parent(); - await validateUserOrRedirect(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath: basePath, isRoot: true }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/[...file]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/[...file]/+page.ts deleted file mode 100644 index 86e8e47c5..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/[...file]/+page.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryPath, getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, params }) => { - await validateUserOrRedirect(); - let slug = params.file as string; - - // split slug with '/' - let slugs = slug.split("/"); - - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - // join all but the last element of the slugs to get the previous path without final "/" - let previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ - metadata.space - }/${metadata.name}/${metadata.version}/files/${slugs - .slice(0, slugs.length - 1) - .join("/")}`.replace(/\/$/, ""); - - // add the rest of the slugs to the basePath - basePath = `${basePath}/${slugs.join("/")}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath, isRoot: false }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.ts deleted file mode 100644 index f9615be51..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getRawFile } from "$lib/components/files/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, url }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - const viewPath = (url as URL).searchParams.get("path") as string; - - let rawFile = await getRawFile(viewPath, metadata.uid, registryType); - let splitPath = viewPath.split("/"); - - return { rawFile, viewPath, splitPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.svelte deleted file mode 100644 index ce566c09a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - -
- {#if dataProfile} -
- -
- - {:else} -
-

No Data Profile Found!

-
-

A data profile was not saved with the current DataCard

-
-
- {/if} -
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.ts deleted file mode 100644 index fd32113f8..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/profile/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; - -import { - getDataProfile, - getSortedFeatureNames, -} from "$lib/components/card/data/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - - const { metadata } = await parent(); - - let dataProfile = metadata.metadata.interface_metadata.save_metadata - ?.data_profile_uri - ? await getDataProfile(metadata) - : undefined; - - // get sorted feature names from dataProfile.features - let featureNames: string[] = []; - - if (dataProfile) { - featureNames = getSortedFeatureNames(dataProfile); - } - - return { dataProfile, featureNames }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/readme/+page.ts deleted file mode 100644 index 863d00906..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/readme/+page.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - let content: string = ""; - - if (readme.exists) { - content = readme.readme; - } else { - content = `# DataCard for ${metadata.space}/${metadata.name} - - - -**Summary:** -Provide a concise summary of the data card, its intended use, and any notable features. - ---- - -## Model Details - -- **Space:** ${metadata.space} -- **Name:** ${metadata.name} -- **Version:** ${metadata.version} -- **Registry:** ${registryType} - ---- - -## Description - -Describe the model or dataset in detail. -Include its origin, intended use cases, and any relevant background information. - ---- - -## Contact - -- **Contact Person/Team:** [Add contact information or support email] -- **License:** [Specify license, e.g., MIT, Apache 2.0, etc.] - ---- - -## Data Development - -Explain how the data/model was developed, including sources, methodology, and any preprocessing steps. - ---- - -## Uses - -List and describe common or recommended uses for this data card. - ---- - -## Bias, Risk, and Limitations - - - -Discuss any known biases, risks, or limitations associated with this data/model. -Mention steps taken to mitigate these issues, if any. - ---- - -## Code Examples - - -\`\`\`python -# Example usage in Python -from opsml import CardRegistry -registry = CardRegistry("${registryType.toLowerCase()}") -card = registry.load_card(uid="${metadata.uid}") -\`\`\` - -`; - } - - return { - metadata, - content, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index 4f7dae846..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - // get metric names, parameters - let versionPage = await getVersionPage( - registryType, - metadata.space, - metadata.name - ); - - let space = [metadata.space]; - let versionStats = await getRegistryStats(registryType, metadata.name, space); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.svelte deleted file mode 100644 index c934f0101..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -
- - {@render children()} - -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.ts deleted file mode 100644 index 56195d15c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Experiment, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.ts deleted file mode 100644 index 6fc388eb0..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import { RegistryType } from "$lib/utils"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/[uid]/+page.ts deleted file mode 100644 index a6e8293e2..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/[uid]/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; -export const prerender = false; -import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import type { ExperimentCard } from "$lib/components/card/card_interfaces/experimentcard"; -import { getCardMetadata } from "$lib/components/card/utils"; -import { getRegistryPath } from "$lib/utils"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let metadata = (await getCardMetadata( - undefined, - undefined, - undefined, - params.uid, - registryType - )) as ExperimentCard; - - redirect( - 301, - `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ - metadata.name - }/${metadata.version}/card` - ); -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.ts deleted file mode 100644 index 6788b8e6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.ts deleted file mode 100644 index 3857cc04c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index f9e635a79..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,92 +0,0 @@ - - -
- - - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index 3dfe52065..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getRegistryTypeLowerCase } from "$lib/utils"; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import { getCardReadMe } from "$lib/components/readme/util"; -import type { ExperimentCard } from "$lib/components/card/card_interfaces/experimentcard"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as ExperimentCard; - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let registryPath = getRegistryTypeLowerCase(registryType); - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index f29994bec..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getCardParameters } from "$lib/components/card/experiment/util"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme, registryPath } = await parent(); - - let parameters = await getCardParameters(metadata.uid); - - return { metadata, registryType, readme, registryPath, parameters }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/figures/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/figures/+page.ts deleted file mode 100644 index 9d766171a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/figures/+page.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const ssr = false; - -import { - getExperimentFigures, - getHardwareMetrics, -} from "$lib/components/card/experiment/util"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata } = await parent(); - - // get metric names, parameters - let experimentFigures = await getExperimentFigures( - metadata.uid, - metadata.space, - metadata.name, - metadata.version - ); - - return { figures: experimentFigures }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.svelte deleted file mode 100644 index 737159d98..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.ts deleted file mode 100644 index 67459fe59..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath: basePath, isRoot: true }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.svelte deleted file mode 100644 index f20625802..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.ts deleted file mode 100644 index 86e8e47c5..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/[...file]/+page.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryPath, getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, params }) => { - await validateUserOrRedirect(); - let slug = params.file as string; - - // split slug with '/' - let slugs = slug.split("/"); - - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - // join all but the last element of the slugs to get the previous path without final "/" - let previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ - metadata.space - }/${metadata.name}/${metadata.version}/files/${slugs - .slice(0, slugs.length - 1) - .join("/")}`.replace(/\/$/, ""); - - // add the rest of the slugs to the basePath - basePath = `${basePath}/${slugs.join("/")}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath, isRoot: false }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.svelte deleted file mode 100644 index aefa8b439..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
-
- -
-
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.ts deleted file mode 100644 index 2c7231afe..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/files/view/+page.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getRawFile } from "$lib/components/files/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, url }) => { - await validateUserOrRedirect(); - const { metadata } = await parent(); - const viewPath = (url as URL).searchParams.get("path") as string; - - let rawFile = await getRawFile( - viewPath, - metadata.uid, - metadata.registry_type - ); - let splitPath = viewPath.split("/"); - - return { rawFile, viewPath, splitPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.ts deleted file mode 100644 index 38ae4c4a8..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/hardware/+page.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const ssr = false; - -import { getHardwareMetrics } from "$lib/components/card/experiment/util"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata } = await parent(); - - // get metric names, parameters - let hardwareMetrics = await getHardwareMetrics(metadata.uid); - - return { hardwareMetrics }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.ts deleted file mode 100644 index e394b2441..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/metrics/+page.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const ssr = false; - -import { - getCardMetricNames, - getRecentExperiments, -} from "$lib/components/card/experiment/util"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata } = await parent(); - - // get metric names, parameters - let metricNames = await getCardMetricNames(metadata.uid); - - let recentExperiments = await getRecentExperiments( - metadata.space, - metadata.name, - metadata.version - ); - - return { metadata, metricNames, recentExperiments }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/readme/+page.ts deleted file mode 100644 index 5a467c0a2..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/readme/+page.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - let content: string = ""; - - if (readme.exists) { - content = readme.readme; - } else { - content = `# ExperimentCard for ${metadata.space}/${metadata.name} - - - -**Summary:** -Provide a concise summary of the experiment card, its intended use, and any notable features. - ---- - -## Experiment Details - -- **Space:** ${metadata.space} -- **Name:** ${metadata.name} -- **Version:** ${metadata.version} -- **Registry:** ${registryType} - ---- - -## Description - -Describe the experiment in detail. -Include its origin, intended use cases, and any relevant background information. - ---- - -## Experiment Development - -Explain how the experiment was developed, including sources, methodology, and any preprocessing steps. - ---- - -## Code Examples - - -\`\`\`python -# Example usage in Python -from opsml import CardRegistry -registry = CardRegistry("${registryType.toLowerCase()}") -card = registry.load_card(uid="${metadata.uid}") -\`\`\` - -`; - } - - return { - metadata, - content, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index e5c06173b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let [versionPage, versionStats] = await Promise.all([ - getVersionPage(registryType, metadata.space, metadata.name), - getRegistryStats(registryType, metadata.name, [metadata.space]), - ]); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+layout.ts deleted file mode 100644 index a56b576a3..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Prompt, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.svelte index 375735755..a1b8019b0 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.svelte @@ -1,5 +1,4 @@ diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.ts deleted file mode 100644 index 5f3910462..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/+page.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -// @ts-ignore -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.server.ts new file mode 100644 index 000000000..25b1938d0 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.server.ts @@ -0,0 +1,9 @@ +import type { LayoutServerLoad } from "./$types"; +import { getRegistryFromString } from "$lib/utils"; + +export const load: LayoutServerLoad = async ({ params }) => { + let registryType = getRegistryFromString(params.registry); + return { + registryType, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.svelte new file mode 100644 index 000000000..ffae99c18 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+layout.svelte @@ -0,0 +1,10 @@ + + +
+ {#key page.params.registry} + {@render children()} + {/key} +
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.server.ts new file mode 100644 index 000000000..ea4ee2b16 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.server.ts @@ -0,0 +1,34 @@ +import { setupRegistryPage } from "$lib/server/card/utils"; +import { getRegistryFromString, RegistryType } from "$lib/utils"; +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad, EntryGenerator } from "./$types"; + +export const entries: EntryGenerator = () => { + return [ + { registry: "model" }, + { registry: "data" }, + { registry: "experiment" }, + { registry: "service" }, + ]; +}; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + let { registryType } = await parent(); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } + + let registryPage = await setupRegistryPage( + registryType, + undefined, + undefined, + fetch + ); + + return { + page: registryPage, + selectedSpace: undefined, + selectedName: undefined, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.svelte new file mode 100644 index 000000000..e80b5cd59 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/+page.svelte @@ -0,0 +1,20 @@ + + +{#if data.registryType === RegistryType.Agent} + +{:else} + +{/if} + + + diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/[uid]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/[uid]/+page.server.ts new file mode 100644 index 000000000..80de0f0d4 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/[uid]/+page.server.ts @@ -0,0 +1,32 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { getRegistryTypeLowerCase } from "$lib/utils"; +import type { ServiceCard } from "$lib/components/card/card_interfaces/servicecard"; +import { getCardMetadata } from "$lib/server/card/utils"; +import type { BaseCard } from "$lib/components/home/types"; + +export const load: PageServerLoad = async ({ parent, params, fetch }) => { + let { registryType } = await parent(); + + console.log("uid param:", params.uid); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } + + let metadata = (await getCardMetadata( + undefined, + undefined, + undefined, + params.uid, + registryType, + fetch + )) as BaseCard; + + redirect( + 301, + `/opsml/${getRegistryTypeLowerCase(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}/card` + ); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.server.ts new file mode 100644 index 000000000..2d5950bff --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.server.ts @@ -0,0 +1,20 @@ +import { setupRegistryPage } from "$lib/server/card/utils"; +import type { PageServerLoad } from "./$types"; +import { redirect } from "@sveltejs/kit"; + +export const load: PageServerLoad = async ({ parent, params, fetch }) => { + const space = params.space; + const name = undefined; // No name parameter in this route + let { registryType } = await parent(); + + if (!registryType) { + throw redirect(307, "/opsml/home"); + } + + let registryPage = await setupRegistryPage(registryType, space, name, fetch); + return { + page: registryPage, + selectedSpace: space, + selectedName: name, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.svelte similarity index 81% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.svelte index 512d359b7..b802292c5 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/+page.svelte @@ -1,7 +1,7 @@ + + + + {@render children()} + \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/card/+page.svelte new file mode 100644 index 000000000..5d42bde2b --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/card/+page.svelte @@ -0,0 +1,24 @@ + + +{#if registryType === RegistryType.Prompt} + +{:else if registryType === RegistryType.Mcp} + +{:else} +
+
+

Unknown registry type: {registryType}

+
+
+{/if} + diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.server.ts new file mode 100644 index 000000000..647de28a6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.server.ts @@ -0,0 +1,13 @@ +import type { PageServerLoad } from "./$types"; +import { getFileTree } from "$lib/server/card/files/utils"; +import { getRegistryTableName } from "$lib/utils"; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + const { metadata, registryType } = await parent(); + + let tableName = getRegistryTableName(registryType); + let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; + let fileTree = await getFileTree(fetch, basePath); + + return { fileTree, previousPath: basePath, isRoot: true }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.svelte similarity index 97% rename from crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.svelte index 6e0fa7a5d..930e00f35 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/+page.svelte @@ -10,7 +10,7 @@
- { + return await loadFileTree(args); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/[...file]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/[...file]/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/[...file]/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts new file mode 100644 index 000000000..ae92372e6 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.server.ts @@ -0,0 +1,13 @@ +import type { PageServerLoad } from "./$types"; +import { getRawFile } from "$lib/server/card/files/utils"; +import { RegistryType } from "$lib/utils"; + +export const load: PageServerLoad = async ({ parent, url, fetch }) => { + const { registryType, metadata } = await parent(); + const viewPath = (url as URL).searchParams.get("path") as string; + + let rawFile = await getRawFile(fetch, viewPath, metadata.uid, registryType); + let splitPath = viewPath.split("/"); + + return { rawFile, viewPath, splitPath }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.svelte similarity index 98% rename from crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.svelte index 07d91dfd8..248f0ea31 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/data/card/[space]/[name]/[version]/files/view/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/files/view/+page.svelte @@ -19,4 +19,4 @@ version={data.metadata.version} />
-
\ No newline at end of file +
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte new file mode 100644 index 000000000..e0911b72c --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.svelte @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts new file mode 100644 index 000000000..aeda26b23 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/monitoring/+page.ts @@ -0,0 +1,27 @@ +// Base data for monitoring page. +// This needs to be client-side because we need to calculate max data points from window size +export const ssr = false; + +import type { PageLoad } from "./$types"; +import { TimeInterval } from "$lib/components/card/monitoring/types"; +import { loadMonitoringDashboardData } from "$lib/components/card/monitoring/getMonitoringDashboardData"; +import { getRegistryPath, RegistryType } from "$lib/utils"; +import { goto } from "$app/navigation"; + +export const load: PageLoad = async ({ parent, fetch }) => { + const { registryType, metadata } = await parent(); + + // redirect to card layout if registryType is not 'prompt' + if (registryType !== RegistryType.Prompt) { + throw goto( + `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ + metadata.name + }/${metadata.version}` + ); + } + return loadMonitoringDashboardData(fetch, await parent(), { + initialTimeInterval: TimeInterval.SixHours, + loadLLMRecords: true, + loadAlerts: true, + }); +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/readme/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/readme/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/readme/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/readme/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/readme/+page.ts new file mode 100644 index 000000000..02003744e --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/readme/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from "./$types"; +import { generateReadmeContent } from "$lib/components/readme/readmeGenerator"; + +export const load: PageLoad = async ({ parent }) => { + const { metadata, registryType, readme } = await parent(); + + const content = generateReadmeContent(registryType, metadata, readme); + + return { + metadata, + content, + }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts new file mode 100644 index 000000000..f295f088b --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/versions/+page.server.ts @@ -0,0 +1,13 @@ +import { getRegistryStats, getVersionPage } from "$lib/server/card/utils"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ parent, fetch }) => { + const { metadata, registryType } = await parent(); + + let [versionPage, versionStats] = await Promise.all([ + getVersionPage(fetch, registryType, metadata.space, metadata.name), + getRegistryStats(fetch, registryType, metadata.name, [metadata.space]), + ]); + + return { versionPage, versionStats }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/versions/+page.svelte similarity index 100% rename from crates/opsml_server/opsml_ui/src/routes/opsml/experiment/card/[space]/[name]/[version]/versions/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/genai/[registry]/card/[space]/[name]/[version]/versions/+page.svelte diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.svelte deleted file mode 100644 index 3919da045..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- {@render children()} -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.ts deleted file mode 100644 index a56b576a3..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/agent/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Prompt, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.svelte deleted file mode 100644 index d1b71b732..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- {@render children()} -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.ts deleted file mode 100644 index c1a3853ad..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Mcp, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.ts deleted file mode 100644 index 5f3910462..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/+page.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -// @ts-ignore -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.ts deleted file mode 100644 index 494ef9c46..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -// @ts-ignore -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.ts deleted file mode 100644 index 019b6d06f..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -// @ts-ignore -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index a919095bb..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,60 +0,0 @@ - - -
-
-
-

- - -
/
-
{data.metadata.version}
-

- - -
-
- - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index b91814a54..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getRegistryPath, getRegistryTypeLowerCase } from "$lib/utils"; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import { getCardReadMe } from "$lib/components/readme/util"; -import type { ServiceCard } from "$lib/components/card/card_interfaces/servicecard"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as ServiceCard; - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.svelte deleted file mode 100644 index a5bf69147..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.svelte +++ /dev/null @@ -1,67 +0,0 @@ - - - -
- -
- -
- -
- - {#if service.cards && service.cards.cards.length > 0} -
-
-
- - - Card List - - -
- {#each service.cards.cards as card} - - {/each} -
- - {/if} - - - {#if deploymentConfig && deploymentConfig.length > 0} -
- {#each deploymentConfig as config} -
- -
- {/each} -
- {/if} - - - - - \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index 9e935269e..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - return { metadata, registryType, readme }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.svelte deleted file mode 100644 index 23a0b60da..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index c907e1ee6..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/mcp/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let [versionPage, versionStats] = await Promise.all([ - getVersionPage(registryType, metadata.space, metadata.name), - getRegistryStats(registryType, metadata.name, [metadata.space]), - ]); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.svelte deleted file mode 100644 index d1b71b732..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- {@render children()} -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.ts deleted file mode 100644 index a56b576a3..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Prompt, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.ts deleted file mode 100644 index b91831c6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/+page.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/[uid]/+page.ts deleted file mode 100644 index b0cc4b4c4..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/[uid]/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; -export const prerender = false; -import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import type { PromptCard } from "$lib/components/card/card_interfaces/promptcard"; -import { getCardMetadata } from "$lib/components/card/utils"; -import { getRegistryTypeLowerCase } from "$lib/utils"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let metadata = (await getCardMetadata( - undefined, - undefined, - undefined, - params.uid, - registryType - )) as PromptCard; - - redirect( - 301, - `/opsml/${getRegistryTypeLowerCase(registryType)}/card/${metadata.space}/${ - metadata.name - }/${metadata.version}` - ); -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.ts deleted file mode 100644 index 6788b8e6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.ts deleted file mode 100644 index 3857cc04c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index 017e2fbc8..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - -
-
-
-

- - -
/
-
{data.metadata.version}
-

- -
- - - Card - - - {#if data.metadata.metadata.drift_profile_uri_map && uiSettingsStore.scouterEnabled} - - - Monitoring - - {/if} - - - - Files - - - - - Versions - -
-
-
- - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index b0db99075..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import { getCardReadMe } from "$lib/components/readme/util"; -import type { PromptCard } from "$lib/components/card/card_interfaces/promptcard"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as PromptCard; - - console.log("Prompt Card Metadata:", metadata); - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index 38728fa66..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - return { metadata, registryType, readme }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.svelte deleted file mode 100644 index 737159d98..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.ts deleted file mode 100644 index ab11d84c7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryPath, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath: basePath, isRoot: true, registryPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.svelte deleted file mode 100644 index f20625802..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.ts deleted file mode 100644 index 1b06a0bea..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/[...file]/+page.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryPath, getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, params }) => { - await validateUserOrRedirect(); - let slug = params.file as string; - - // split slug with '/' - let slugs = slug.split("/"); - - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - // join all but the last element of the slugs to get the previous path without final "/" - let previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ - metadata.space - }/${metadata.name}/${metadata.version}/files/${slugs - .slice(0, slugs.length - 1) - .join("/")}`.replace(/\/$/, ""); - - // add the rest of the slugs to the basePath - basePath = `${basePath}/${slugs.join("/")}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath, isRoot: false, registryType }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.svelte deleted file mode 100644 index ee5caa798..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.ts deleted file mode 100644 index f9615be51..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/files/view/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getRawFile } from "$lib/components/files/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, url }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - const viewPath = (url as URL).searchParams.get("path") as string; - - let rawFile = await getRawFile(viewPath, metadata.uid, registryType); - let splitPath = viewPath.split("/"); - - return { rawFile, viewPath, splitPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.svelte deleted file mode 100644 index 77e52ef46..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.svelte +++ /dev/null @@ -1,211 +0,0 @@ - - -
-
- - - -
-
-
- - -
- - {#if currentName && latestMetrics} - {#if currentMetricData} - - {:else} -
- No data available for selected metric -
- {/if} - {:else} -
- Select a metric to view data -
- {/if} -
- -
- -
- -
- -
- - -
-
- \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.ts deleted file mode 100644 index 339a1292a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/monitoring/+page.ts +++ /dev/null @@ -1,98 +0,0 @@ -export const ssr = false; - -import { getMaxDataPoints } from "$lib/utils"; -import type { PageLoad } from "./$types"; -import { - getDriftProfiles, - getProfileConfig, - getProfileFeatures, - type UiProfile, -} from "$lib/components/card/monitoring/util"; -import { DriftType, TimeInterval } from "$lib/components/card/monitoring/types"; -import { - getLatestMetrics, - getCurrentMetricData, -} from "$lib/components/card/monitoring/util"; -import { getDriftAlerts } from "$lib/components/card/monitoring/alert/utils"; -import { getLLMRecordPage } from "$lib/components/card/monitoring/util"; -import type { ServiceInfo } from "$lib/components/card/monitoring/types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let profiles = await getDriftProfiles( - metadata.uid, - metadata.metadata.drift_profile_uri_map ?? {}, - registryType - ); - - // get all keys which should be of DriftType - const keys: DriftType[] = Object.keys(profiles) - .filter((key): key is DriftType => { - return Object.values(DriftType).includes(key as DriftType); - }) - .sort(); - - let currentDriftType = keys[0]; - let currentProfile: UiProfile = profiles[currentDriftType]; - let currentNames: string[] = getProfileFeatures( - currentDriftType, - currentProfile.profile - ); - let currentName: string = currentNames[0]; - let currentConfig = getProfileConfig( - currentDriftType, - currentProfile.profile - ); - let maxDataPoints = getMaxDataPoints(); - - let latestMetrics = await getLatestMetrics( - profiles, - TimeInterval.SixHours, - maxDataPoints - ); - - // Filter latest metrics to the current drift type - let currentMetricData = getCurrentMetricData( - latestMetrics, - currentDriftType, - currentName - ); - - let currentAlerts = await getDriftAlerts( - currentConfig.space, - currentConfig.name, - currentConfig.version, - TimeInterval.SixHours, - true - ); - - let service_info: ServiceInfo = { - space: currentConfig.space, - name: currentConfig.name, - version: currentConfig.version, - }; - - let currentLLMRecords = await getLLMRecordPage( - service_info, - undefined, - undefined - ); - - return { - profiles, - keys, - currentName, - currentNames, - currentDriftType, - currentProfile, - currentConfig, - latestMetrics, - currentMetricData, - maxDataPoints, - currentAlerts, - currentLLMRecords, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.svelte deleted file mode 100644 index 575f90d49..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-
-
- -
-
-
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.ts deleted file mode 100644 index f068d92ef..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/readme/+page.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme, registryPath } = await parent(); - - let content: string = ""; - - if (readme.exists) { - content = readme.readme; - } else { - content = `# Prompt Card for ${metadata.space}/${metadata.name} - -**Summary:** -Briefly describe what this prompt does and its main purpose. - ---- - -## Details - -- **Space:** ${metadata.space} -- **Name:** ${metadata.name} -- **Version:** ${metadata.version} -- **Registry:** ${registryType} - ---- - -## Description - -Describe the prompt in detail, including its intended use, context, and any relevant background. - ---- - -## Development Notes - -Explain how the prompt was created, including any sources, methodology, or special considerations. - ---- - -## Example Usage - -\`\`\`python -from opsml import CardRegistry -registry = CardRegistry("${registryPath}") -card = registry.load_card(uid="${metadata.uid}") -\`\`\` -`; - } - - return { - metadata, - content, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.svelte deleted file mode 100644 index 23a0b60da..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index e5c06173b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/genai/prompt/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let [versionPage, versionStats] = await Promise.all([ - getVersionPage(registryType, metadata.space, metadata.name), - getRegistryStats(registryType, metadata.name, [metadata.space]), - ]); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.server.ts new file mode 100644 index 000000000..a1a356932 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.server.ts @@ -0,0 +1,32 @@ +import { getRegistryStats } from "$lib/server/card/utils"; +import { + getRecentCards, + type HomePageStats, +} from "$lib/components/home/utils.server"; +import { RegistryType } from "$lib/utils"; +import type { PageServerLoad } from "./$types"; + +async function get_registry_stats( + fetch: typeof globalThis.fetch +): Promise { + const [modelStats, dataStats, promptStats, experimentStats] = + await Promise.all([ + getRegistryStats(fetch, RegistryType.Model), + getRegistryStats(fetch, RegistryType.Data), + getRegistryStats(fetch, RegistryType.Prompt), + getRegistryStats(fetch, RegistryType.Experiment), + ]); + + return { + nbrModels: modelStats.stats.nbr_names, + nbrData: dataStats.stats.nbr_names, + nbrPrompts: promptStats.stats.nbr_names, + nbrExperiments: experimentStats.stats.nbr_names, + }; +} + +export const load: PageServerLoad = async ({ fetch }) => { + let cards = await getRecentCards(fetch); + let stats = await get_registry_stats(fetch); + return { cards, stats }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.svelte index 9b594411d..2098d7518 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/home/+page.svelte @@ -1,7 +1,7 @@ - -
- - {@render children()} - -
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/+layout.ts deleted file mode 100644 index 5353b560d..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Model, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.ts deleted file mode 100644 index b91831c6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/+page.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/[uid]/+page.ts deleted file mode 100644 index 1abbf13dc..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/[uid]/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; -export const prerender = false; -import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import type { ModelCard } from "$lib/components/card/card_interfaces/modelcard"; -import { getCardMetadata } from "$lib/components/card/utils"; -import { getRegistryPath } from "$lib/utils"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let metadata = (await getCardMetadata( - undefined, - undefined, - undefined, - params.uid, - registryType - )) as ModelCard; - - redirect( - 301, - `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ - metadata.name - }/${metadata.version}/card` - ); -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.ts deleted file mode 100644 index 6788b8e6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.ts deleted file mode 100644 index 3857cc04c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index 1f3d56b1b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
-
-
-

- - -
/
-
{data.metadata.version}
-

- -
- - - Card - - - - Files - - {#if data.metadata.metadata.interface_metadata.save_metadata.drift_profile_uri_map && uiSettingsStore.scouterEnabled} - - - Monitoring - - {/if} - - - Versions - -
-
-
- - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index 75a8fcc3c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getRegistryTypeLowerCase } from "$lib/utils"; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import type { ModelCard } from "$lib/components/card/card_interfaces/modelcard"; -import { getCardReadMe } from "$lib/components/readme/util"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as ModelCard; - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let registryPath = getRegistryTypeLowerCase(registryType); - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index 38728fa66..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - return { metadata, registryType, readme }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/+page.ts deleted file mode 100644 index 67459fe59..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath: basePath, isRoot: true }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/[...file]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/[...file]/+page.ts deleted file mode 100644 index 86e8e47c5..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/[...file]/+page.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryPath, getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, params }) => { - await validateUserOrRedirect(); - let slug = params.file as string; - - // split slug with '/' - let slugs = slug.split("/"); - - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - // join all but the last element of the slugs to get the previous path without final "/" - let previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ - metadata.space - }/${metadata.name}/${metadata.version}/files/${slugs - .slice(0, slugs.length - 1) - .join("/")}`.replace(/\/$/, ""); - - // add the rest of the slugs to the basePath - basePath = `${basePath}/${slugs.join("/")}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath, isRoot: false }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/view/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/view/+page.ts deleted file mode 100644 index 16bca4ac0..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/files/view/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getRawFile } from "$lib/components/files/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import { RegistryType } from "$lib/utils"; - -export const load: PageLoad = async ({ parent, url }) => { - await validateUserOrRedirect(); - const { metadata } = await parent(); - const viewPath = (url as URL).searchParams.get("path") as string; - - let rawFile = await getRawFile(viewPath, metadata.uid, RegistryType.Model); - let splitPath = viewPath.split("/"); - - return { rawFile, viewPath, splitPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.svelte deleted file mode 100644 index aad88b18b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.svelte +++ /dev/null @@ -1,199 +0,0 @@ - - -
-
- - - -
-
-
- - -
- - {#if currentName && latestMetrics} - {#if currentMetricData} - - {:else} -
- No data available for selected metric -
- {/if} - {:else} -
- Select a metric to view data -
- {/if} -
- - -
- -
- - - - -
-
- \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.ts deleted file mode 100644 index 19326723e..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/monitoring/+page.ts +++ /dev/null @@ -1,86 +0,0 @@ -export const ssr = false; - -import { getMaxDataPoints } from "$lib/utils"; -import type { PageLoad } from "./$types"; -import { - getDriftProfiles, - getProfileConfig, - getProfileFeatures, - type UiProfile, -} from "$lib/components/card/monitoring/util"; -import { DriftType, TimeInterval } from "$lib/components/card/monitoring/types"; -import { - // getLatestMetricsExample, - getLatestMetrics, - getCurrentMetricData, -} from "$lib/components/card/monitoring/util"; -import { getDriftAlerts } from "$lib/components/card/monitoring/alert/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let profiles = await getDriftProfiles( - metadata.uid, - metadata.metadata.interface_metadata.save_metadata.drift_profile_uri_map ?? - {}, - registryType - ); - - // get all keys which should be of DriftType - const keys: DriftType[] = Object.keys(profiles) - .filter((key): key is DriftType => { - return Object.values(DriftType).includes(key as DriftType); - }) - .sort(); - - let currentDriftType = keys[0]; - let currentProfile: UiProfile = profiles[currentDriftType]; - let currentNames: string[] = getProfileFeatures( - currentDriftType, - currentProfile.profile - ); - let currentName: string = currentNames[0]; - let currentConfig = getProfileConfig( - currentDriftType, - currentProfile.profile - ); - let maxDataPoints = getMaxDataPoints(); - - // get latest metrics for all available drift profiles - let latestMetrics = await getLatestMetrics( - profiles, - TimeInterval.SixHours, - maxDataPoints - ); - - // Filter latest metrics to the current drift type - let currentMetricData = getCurrentMetricData( - latestMetrics, - currentDriftType, - currentName - ); - - let currentAlerts = await getDriftAlerts( - currentConfig.space, - currentConfig.name, - currentConfig.version, - TimeInterval.SixHours, - true - ); - - return { - profiles, - keys, - currentName, - currentNames, - currentDriftType, - currentProfile, - currentConfig, - latestMetrics, - currentMetricData, - maxDataPoints, - currentAlerts, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.svelte deleted file mode 100644 index 575f90d49..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-
-
- -
-
-
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.ts deleted file mode 100644 index e403ec470..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/readme/+page.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - let content: string = ""; - - if (readme.exists) { - content = readme.readme; - } else { - content = `# ModelCard for ${metadata.space}/${metadata.name} - - - -**Summary:** -Provide a concise summary of the model card, its intended use, and any notable features. - ---- - -## Model Details - -- **Space:** ${metadata.space} -- **Name:** ${metadata.name} -- **Version:** ${metadata.version} -- **Registry:** ${registryType} - ---- - -## Description - -Describe the model in detail. -Include its origin, intended use cases, and any relevant background information. - ---- - -## Contact - -- **Contact Person/Team:** [Add contact information or support email] -- **License:** [Specify license, e.g., MIT, Apache 2.0, etc.] - ---- - -## Model Development - -Explain how the model was developed, including sources, methodology, and any preprocessing steps. - ---- - -## Uses - -List and describe common or recommended uses for this model card. - ---- - -## Bias, Risk, and Limitations - -Discuss any known biases, risks, or limitations associated with this model. -Mention steps taken to mitigate these issues, if any. - ---- - -## Code Examples - - -\`\`\`python -# Example usage in Python -from opsml import CardRegistry -registry = CardRegistry("${registryType.toLowerCase()}") -card = registry.load_card(uid="${metadata.uid}") -\`\`\` - -`; - } - - return { - metadata, - content, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.svelte deleted file mode 100644 index 23a0b60da..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index e5c06173b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/model/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let [versionPage, versionStats] = await Promise.all([ - getVersionPage(registryType, metadata.space, metadata.name), - getRegistryStats(registryType, metadata.name, [metadata.space]), - ]); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.ts deleted file mode 100644 index 45285da2a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+layout.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { RegistryType } from "$lib/utils"; -import type { LayoutLoad } from "./$types"; - -export const load: LayoutLoad = async ({}) => { - return { - registryType: RegistryType.Service, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.ts deleted file mode 100644 index b91831c6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/+page.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const ssr = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let registryPage = await setupRegistryPage( - registryType, - undefined, - undefined - ); - return { - page: registryPage, - selectedSpace: undefined, - selectedName: undefined, - }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/[uid]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/[uid]/+page.ts deleted file mode 100644 index 8984a4165..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/[uid]/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const ssr = false; -export const prerender = false; -import { redirect } from "@sveltejs/kit"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import type { ServiceCard } from "$lib/components/card/card_interfaces/servicecard"; -import { getCardMetadata } from "$lib/components/card/utils"; -import { getRegistryPath } from "$lib/utils"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - let metadata = (await getCardMetadata( - undefined, - undefined, - undefined, - params.uid, - registryType - )) as ServiceCard; - - redirect( - 301, - `/opsml/${getRegistryPath(registryType)}/card/${metadata.space}/${ - metadata.name - }/${metadata.version}/card` - ); -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.ts deleted file mode 100644 index 6788b8e6b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = undefined; // No name parameter in this route - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.svelte deleted file mode 100644 index 512d359b7..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.ts deleted file mode 100644 index 3857cc04c..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import { setupRegistryPage } from "$lib/components/card/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - - const space = params.space; - const name = params.name; - - let registryPage = await setupRegistryPage(registryType, space, name); - return { page: registryPage, selectedSpace: space, selectedName: name }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.svelte deleted file mode 100644 index 50fc73dab..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -
-
-
-

- - -
/
-
{data.metadata.version}
-

- - -
-
- - -
- {@render children()} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.ts deleted file mode 100644 index b91814a54..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/+layout.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const prerender = false; -export const ssr = false; -import { getRegistryPath, getRegistryTypeLowerCase } from "$lib/utils"; -import { getCardMetadata } from "$lib/components/card/utils"; - -import type { LayoutLoad } from "./$types"; -import { getCardReadMe } from "$lib/components/readme/util"; -import type { ServiceCard } from "$lib/components/card/card_interfaces/servicecard"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: LayoutLoad = async ({ params, parent }) => { - await validateUserOrRedirect(); - const { registryType } = await parent(); - const { space, name, version } = params; - - let metadata = (await getCardMetadata( - space, - name, - version, - undefined, - registryType - )) as ServiceCard; - - let readme = await getCardReadMe(metadata.name, metadata.space, registryType); - - let activeTab = "card"; // Default active tab - - return { metadata, registryType, readme, activeTab }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.ts deleted file mode 100644 index 9e935269e..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/card/+page.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -// @ts-ignore -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType, readme } = await parent(); - - return { metadata, registryType, readme }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.svelte deleted file mode 100644 index 737159d98..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.ts deleted file mode 100644 index 67459fe59..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ssr = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath: basePath, isRoot: true }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.svelte deleted file mode 100644 index a38cf8d7a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
-
- -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.ts deleted file mode 100644 index 86e8e47c5..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/[...file]/+page.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getFileTree } from "$lib/components/files/utils"; -import { getRegistryPath, getRegistryTableName } from "$lib/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, params }) => { - await validateUserOrRedirect(); - let slug = params.file as string; - - // split slug with '/' - let slugs = slug.split("/"); - - const { metadata, registryType } = await parent(); - - let tableName = getRegistryTableName(registryType); - let basePath = `${tableName}/${metadata.space}/${metadata.name}/v${metadata.version}`; - - // join all but the last element of the slugs to get the previous path without final "/" - let previousPath = `/opsml/${getRegistryPath(registryType)}/card/${ - metadata.space - }/${metadata.name}/${metadata.version}/files/${slugs - .slice(0, slugs.length - 1) - .join("/")}`.replace(/\/$/, ""); - - // add the rest of the slugs to the basePath - basePath = `${basePath}/${slugs.join("/")}`; - - let fileTree = await getFileTree(basePath); - - return { fileTree, previousPath, isRoot: false }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.svelte deleted file mode 100644 index 30f571e82..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
-
- -
-
diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.ts deleted file mode 100644 index f9615be51..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/files/view/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; -export const prerender = false; - -import type { PageLoad } from "./$types"; -import { getRawFile } from "$lib/components/files/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent, url }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - const viewPath = (url as URL).searchParams.get("path") as string; - - let rawFile = await getRawFile(viewPath, metadata.uid, registryType); - let splitPath = viewPath.split("/"); - - return { rawFile, viewPath, splitPath }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.svelte deleted file mode 100644 index 23a0b60da..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.ts deleted file mode 100644 index e5c06173b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/service/card/[space]/[name]/[version]/versions/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ssr = false; - -import { getRegistryStats, getVersionPage } from "$lib/components/card/utils"; -import type { PageLoad } from "./$types"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - const { metadata, registryType } = await parent(); - - let [versionPage, versionStats] = await Promise.all([ - getVersionPage(registryType, metadata.space, metadata.name), - getRegistryStats(registryType, metadata.name, [metadata.space]), - ]); - - return { versionPage, versionStats }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.server.ts new file mode 100644 index 000000000..7f2a8a26d --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.server.ts @@ -0,0 +1,11 @@ +export const ssr = false; + +import { getAllSpaceStats } from "$lib/server/space/utils"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ fetch }) => { + // get space for url if exists + let spaces = await getAllSpaceStats(fetch); + + return { spaces }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.ts deleted file mode 100644 index b9015bd3b..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/space/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const ssr = false; - -import { getAllSpaceStats } from "$lib/components/space/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({}) => { - await validateUserOrRedirect(); - - // get space for url if exists - let spaces = await getAllSpaceStats(); - - return { spaces }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.server.ts new file mode 100644 index 000000000..5defb6d38 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.server.ts @@ -0,0 +1,11 @@ +import { getSpace } from "$lib/server/space/utils"; +import type { PageServerLoad } from "./$types"; +import { getRecentCards } from "$lib/components/home/utils.server"; + +export const load: PageServerLoad = async ({ params, fetch }) => { + // get space for url if exists + let spaceRecord = await getSpace(fetch, params.space); + let recentCards = await getRecentCards(fetch, params.space); + + return { spaceRecord, recentCards }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.svelte index 1564df424..558ccd739 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.svelte @@ -1,11 +1,13 @@ diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.ts deleted file mode 100644 index f533e324a..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/space/[space]/+page.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const prerender = false; - -import { getSpace } from "$lib/components/space/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; -import type { PageLoad } from "./$types"; -import { getRecentCards } from "$lib/components/home/utils"; - -export const load: PageLoad = async ({ params }) => { - await validateUserOrRedirect(); - - // get space for url if exists - let spaceRecord = await getSpace(params.space); - let recentCards = await getRecentCards(params.space); - - return { spaceRecord, recentCards }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.svelte index 836657410..69a69466b 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.svelte @@ -3,13 +3,17 @@ import { goto } from "$app/navigation"; import logo from '$lib/images/opsml-logo-small.webp' import LoginWarning from "$lib/components/user/LoginWarning.svelte"; - import { UiPaths } from "$lib/components/api/routes"; + import { UiPaths, ServerPaths } from "$lib/components/api/routes"; import { goTop } from "$lib/utils"; - import { userStore } from "$lib/components/user/user.svelte"; import type { PageProps } from './$types'; import { validateLoginSchema, type UseLoginSchema } from "$lib/components/user/schema"; - import { getSsoAuthURL } from "$lib/components/user/utils"; -import { uiSettingsStore } from "$lib/components/settings/settings.svelte"; + import { uiSettingsStore } from "$lib/components/settings/settings.svelte"; + import { createInternalApiClient } from "$lib/api/internalClient"; + import { userStore } from "$lib/components/user/user.svelte"; + import type { LoginResponse, SsoAuthUrl } from "$lib/components/user/types"; + + + let { previousPath, fetch }: PageProps = $props(); let username: string = $state(''); let password: string = $state(''); @@ -18,46 +22,53 @@ import { uiSettingsStore } from "$lib/components/settings/settings.svelte"; let loginErrors = $state>>({}); - - let { data }: PageProps = $props(); - let previousPath = data.previousPath; - async function handleLogin() { - // Handle login logic here + // Validate input fields + const argsValid = validateLoginSchema(username, password); - let argsValid = validateLoginSchema(username, password); + if (!argsValid.success) { + showLoginError = true; + loginErrors = argsValid.errors ?? {}; + goTop(); + return; + } - if (argsValid.success) { - - let loginResponse = await userStore.login(username, password); - - if (loginResponse.authenticated === true) { - // need to reload the page to update the nav bar - if (previousPath) { - goto(previousPath); - } else { - goto(UiPaths.HOME); - } + try { + // Send login request to server endpoint + const res = await createInternalApiClient(fetch).post(ServerPaths.LOGIN, { username, password }); + const result = await res.json(); + + if (res.ok && result.success) { + // On success, update userStore, redirect to previousPath or home + const loginResponse = result.response as LoginResponse; + userStore.fromLoginResponse(loginResponse); + goto(previousPath ?? UiPaths.HOME); } else { showLoginError = true; - errorMessage = loginResponse.message; + errorMessage = result.error ?? "Invalid username or password"; + goTop(); } - goTop(); - } else { + } catch (err) { + // Handle network or unexpected errors showLoginError = true; - loginErrors = argsValid.errors ?? {}; + errorMessage = "Login failed. Please try again."; + console.error("Login error:", err); goTop(); } -} + } -async function redirectToSsoUrl() { - - const ssoAuthUrl = await getSsoAuthURL(); - localStorage.setItem("ssoState", ssoAuthUrl.state); - localStorage.setItem("ssoCodeVerifier", ssoAuthUrl.code_verifier); + async function redirectToSsoUrl() { + + const resp = await createInternalApiClient(fetch).get(ServerPaths.SSO_AUTH); + const ssoAuthUrl = (await resp.json() as SsoAuthUrl); + + console.log("SSO Auth URL:", ssoAuthUrl); + + localStorage.setItem("ssoState", ssoAuthUrl.state); + localStorage.setItem("ssoCodeVerifier", ssoAuthUrl.code_verifier); - window.location.href = ssoAuthUrl.url; -} + window.location.href = ssoAuthUrl.url; + } diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.ts index afc837a26..918c0685f 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.ts +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/login/+page.ts @@ -1,10 +1,7 @@ -export const ssr = false; - import { UiPaths } from "$lib/components/api/routes"; import type { PageLoad } from "./$types"; export const load: PageLoad = ({ url }) => { - const currentPath = (url as URL).pathname; let previousPath = (url as URL).searchParams.get("redirect") as | string | undefined; @@ -14,7 +11,6 @@ export const load: PageLoad = ({ url }) => { } return { - currentPath, previousPath, }; }; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/logout/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/logout/+page.svelte index a2c48f2ed..2d826434d 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/logout/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/logout/+page.svelte @@ -1,54 +1,53 @@ -
- - -
+
+
+ - OpsML logo -

Logout

+ OpsML logo +

Logout

-

- You are about to logout. Click the logout button below to proceed. -

+

+ You are about to logout. Click the logout button below to proceed. +

- - {#if showLogoutError} - - {/if} - - -
- - -
- -
\ No newline at end of file + {#if showLogoutError} + + {/if} + +
+ + +
+ +
+ \ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.ts deleted file mode 100644 index 0b93830e8..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { userStore } from "$lib/components/user/user.svelte"; -import { getUser } from "$lib/components/user/utils"; -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async ({ parent }) => { - await validateUserOrRedirect(); - await parent(); - - let userInfo = await getUser(userStore.username); - - return { userInfo }; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.server.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.server.ts new file mode 100644 index 000000000..699276518 --- /dev/null +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from "./$types"; +import { getUser } from "$lib/server/user/utils"; + +export const load: PageServerLoad = async ({ params, cookies, fetch }) => { + const username = params.username ?? cookies.get("username") ?? ""; + const userInfo = await getUser(username, fetch); + return { userInfo }; +}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.svelte similarity index 99% rename from crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.svelte rename to crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.svelte index 10273ed8f..b906df7a9 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/profile/[username]/+page.svelte @@ -12,9 +12,7 @@
-
-
User Profile
@@ -66,6 +64,5 @@ {/each}
{/if} -
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/+page.svelte index c49fe5ec6..8cdf63baf 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/+page.svelte @@ -2,17 +2,15 @@ import { goto } from "$app/navigation"; import logo from "$lib/images/opsml-logo-medium.webp"; import LoginWarning from "$lib/components/user/LoginWarning.svelte"; - import { RoutePaths, UiPaths } from "$lib/components/api/routes"; + import { ServerPaths, UiPaths } from "$lib/components/api/routes"; import { goTop } from "$lib/utils"; - import { opsmlClient } from "$lib/components/api/client.svelte"; - import type { PageProps } from './$types'; + import { createInternalApiClient } from "$lib/api/internalClient"; import { validateUserRegisterSchema, type UserRegisterSchema } from "$lib/components/user/schema"; - import { registerUser } from "$lib/components/user/utils"; import { HelpCircle, Eye, EyeOff } from 'lucide-svelte'; import { userStore } from "$lib/components/user/user.svelte"; + import type { CreateUserUiResponse } from "$lib/components/user/types"; - let username: string = $state(''); let password: string = $state(''); let email: string = $state(''); @@ -39,17 +37,33 @@ let argsValid = validateUserRegisterSchema(username, password, reEnterPassword, email); if (argsValid.success) { - let response = await registerUser(username, password, email); + let res = await createInternalApiClient(fetch).post(ServerPaths.REGISTER_USER, { + username, + password, + email + }); + console.log("Registration response status:", res.status); + + let response = (await res.json() as CreateUserUiResponse); if (response.registered) { + // need to goto the register success page to give user recovery codes userStore.setRecoveryCodes(response.response?.recovery_codes ?? []); goto(UiPaths.REGISTER_SUCCESS); } else { + showLoginError = true; + // check if there's a specific error message from the server + // check for unique constraint violation console.error("Registration error:", response.error); - errorMessage = response.error ?? "Error encountered during registration"; + // if Failed to create user: error returned from database" default to generic message and ask to check with administrator + if (response.error?.includes("Failed to create user: error returned from database")) { + errorMessage = "Registration failed. Username or email may already be in use. Please try again with different credentials or contact the administrator."; + } else { + errorMessage = response.error ?? "Error encountered during registration"; + } } goTop(); @@ -67,11 +81,8 @@
-
-
- OpsML logo

Register a new profile

diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/success/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/success/+page.svelte index 88c8a7aaa..fa213b776 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/success/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/register/success/+page.svelte @@ -5,8 +5,6 @@ import { userStore } from "$lib/components/user/user.svelte"; import CodeModal from "$lib/components/card/CodeModal.svelte"; - - async function gotoLogin() { goto(UiPaths.LOGIN); diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.svelte b/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.svelte index 58e272ca9..64a53d7c1 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.svelte +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.svelte @@ -2,16 +2,11 @@ import { goto } from "$app/navigation"; import logo from "$lib/images/opsml-logo-medium.webp"; import PasswordMessage from "$lib/components/user/PasswordMessage.svelte"; - import { RoutePaths, UiPaths } from "$lib/components/api/routes"; + import { ServerPaths, UiPaths } from "$lib/components/api/routes"; import { goTop } from "$lib/utils"; - import { opsmlClient } from "$lib/components/api/client.svelte"; - import type { PageProps } from './$types'; import { validatePasswordResetSchema, type PasswordResetSchema } from "$lib/components/user/schema"; - import { registerUser, resetUserPassword } from "$lib/components/user/utils"; - import { HelpCircle } from 'lucide-svelte'; - import { userStore } from "$lib/components/user/user.svelte"; - - + import { HelpCircle, Eye, EyeOff } from 'lucide-svelte'; + import { createInternalApiClient } from "$lib/api/internalClient"; let username: string = $state(''); let recoveryCode: string = $state(''); @@ -22,118 +17,179 @@ let resetMessage: string = $state("Password reset successful! You can now log in with your new password."); let showPasswordHelp: boolean = $state(false); let passwordErrors = $state>>({}); + let passwordVisible: boolean = $state(false); + + function togglePasswordHelp() { + showPasswordHelp = !showPasswordHelp; + } + + function togglePasswordVisibility() { + passwordVisible = !passwordVisible; + + } - async function handleReset() { - // Handle password reset logic here + /** + * Handles password reset form submission. + * Validates input, sends reset request, and manages UI feedback. + * @param event Form submit event + */ +async function handleReset(event: Event) { - let argsValid = validatePasswordResetSchema(username, recoveryCode, newPassword); + event.preventDefault(); - if (argsValid.success) { - // assert + + const argsValid = validatePasswordResetSchema(username, recoveryCode, newPassword, confirmPassword); - let response = await resetUserPassword(username, recoveryCode, newPassword); + if (!argsValid.success) { + showPasswordHelp = true; + passwordErrors = argsValid.errors ?? {}; + goTop(); + return; + } + + try { + const res = await createInternalApiClient(fetch).post(ServerPaths.RESET_PASSWORD, { + username, + recovery_code: recoveryCode, + new_password: newPassword + }); + + if (res.ok) { + + showResetMessage = true; + resetMessage = "Password reset successful! You can now log in with your new password."; + await new Promise(resolve => setTimeout(resolve, 2000)); + await goto(UiPaths.LOGIN); - showResetMessage = true; - // sleep for 2 seconds to show the message - await new Promise(resolve => setTimeout(resolve, 2000)); - - await goto(UiPaths.LOGIN); - } else { - showPasswordHelp = true; - passwordErrors = argsValid.errors ?? {}; + + const result = await res.json(); + showResetMessage = true; + resetMessage = result.error ?? "Password reset failed. Please try again."; goTop(); } + + } catch (error) { + showResetMessage = true; + resetMessage = "An unexpected error occurred. Please try again later."; + goTop(); + } } -
- - +
- + OpsML logo -

Reset your password

+

Reset your password

+ {#if showResetMessage} - +
+ +
{/if} -
-
\ No newline at end of file diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.ts deleted file mode 100644 index 84c5c9eb6..000000000 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/reset/+page.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { validateUserOrRedirect } from "$lib/components/user/user.svelte"; - -import type { PageLoad } from "./$types"; - -export const load: PageLoad = async () => { - await validateUserOrRedirect(); - return {}; -}; diff --git a/crates/opsml_server/opsml_ui/src/routes/opsml/user/sso/callback/+page.ts b/crates/opsml_server/opsml_ui/src/routes/opsml/user/sso/callback/+page.ts index c8ef7298a..8687994d9 100644 --- a/crates/opsml_server/opsml_ui/src/routes/opsml/user/sso/callback/+page.ts +++ b/crates/opsml_server/opsml_ui/src/routes/opsml/user/sso/callback/+page.ts @@ -1,35 +1,41 @@ +// we are accessing localStorage, so this page cannot be server-side rendered export const ssr = false; -import { RoutePaths, UiPaths } from "$lib/components/api/routes"; import type { PageLoad } from "./$types"; import { userStore } from "$lib/components/user/user.svelte"; -import { exchangeSsoCallbackCode } from "$lib/components/user/utils"; +import { createInternalApiClient } from "$lib/api/internalClient"; +import type { LoginResponse } from "$lib/components/user/types"; +import { ServerPaths } from "$lib/components/api/routes"; -export const load: PageLoad = async ({ url }) => { +export const load: PageLoad = async ({ url, fetch }) => { const code = (url as URL).searchParams.get("code") as string; const state = (url as URL).searchParams.get("state") as string; - //const storedState = userStore.getSsoState(); - // get local storage state + const storedState = localStorage.getItem("ssoState") || ""; + // validate state and code if (!code || !state || state !== storedState) { throw new Error("Invalid state or missing authorization code"); } let codeVerifier = localStorage.getItem("ssoCodeVerifier") || ""; - let loginResponse = await exchangeSsoCallbackCode(code, codeVerifier); + + // send to server to exchange code for tokens + let resp = await createInternalApiClient(fetch).post( + ServerPaths.SSO_CALLBACK, + { + code, + code_verifier: codeVerifier, + } + ); + + const loginResponse = (await resp.json()) as LoginResponse; if (loginResponse.authenticated) { - localStorage.removeItem("ssoState"); // Clear the stored state after successful login - localStorage.removeItem("ssoCodeVerifier"); // Clear the code verifier after successful login - - userStore.updateUser( - loginResponse.username, - loginResponse.jwt_token, - loginResponse.permissions, - loginResponse.group_permissions, - loginResponse.favorite_spaces - ); + // Clear the stored vars after successful login + localStorage.removeItem("ssoState"); + localStorage.removeItem("ssoCodeVerifier"); + userStore.fromLoginResponse(loginResponse); } return { response: loginResponse }; diff --git a/crates/opsml_server/opsml_ui/svelte.config.js b/crates/opsml_server/opsml_ui/svelte.config.js index 9eefcc4d0..2c865299b 100644 --- a/crates/opsml_server/opsml_ui/svelte.config.js +++ b/crates/opsml_server/opsml_ui/svelte.config.js @@ -1,27 +1,17 @@ -import { mdsvex } from "mdsvex"; -import adapter from "@sveltejs/adapter-static"; +import adapter from "@sveltejs/adapter-node"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: [vitePreprocess(), mdsvex()], + preprocess: [vitePreprocess()], kit: { - paths: { - relative: false, - }, adapter: adapter({ - pages: "site", - assets: "site", - fallback: "index.html", + out: "build", precompress: false, - strict: true, }), }, - - extensions: [".svelte", ".svx"], + extensions: [".svelte"], }; export default config; diff --git a/crates/opsml_server/src/core/mod.rs b/crates/opsml_server/src/core/mod.rs index 9266c2ffe..bb7aed51b 100644 --- a/crates/opsml_server/src/core/mod.rs +++ b/crates/opsml_server/src/core/mod.rs @@ -15,5 +15,4 @@ pub mod settings; pub mod setup; pub mod shutdown; pub mod state; -pub mod ui; pub mod user; diff --git a/crates/opsml_server/src/core/router.rs b/crates/opsml_server/src/core/router.rs index ce0529ddf..8310c3b22 100644 --- a/crates/opsml_server/src/core/router.rs +++ b/crates/opsml_server/src/core/router.rs @@ -11,7 +11,6 @@ use crate::core::middleware::metrics::track_metrics; use crate::core::scouter::route::get_scouter_router; use crate::core::settings::route::get_settings_router; use crate::core::state::AppState; -use crate::core::ui::get_ui_router; use crate::core::user::route::get_user_router; use anyhow::Result; use axum::http::{ @@ -19,6 +18,7 @@ use axum::http::{ Method, }; use axum::{middleware, Router}; +use reqwest::header::HeaderValue; use std::sync::Arc; use tower_http::cors::CorsLayer; @@ -26,6 +26,7 @@ const ROUTE_PREFIX: &str = "/opsml/api"; pub async fn create_router(app_state: Arc) -> Result { let cors = CorsLayer::new() + .allow_origin("http://localhost:3000".parse::()?) .allow_methods([ Method::GET, Method::PUT, @@ -46,7 +47,6 @@ pub async fn create_router(app_state: Arc) -> Result { let user_routes = get_user_router(ROUTE_PREFIX).await?; let scouter_routes = get_scouter_router(ROUTE_PREFIX).await?; let genai_routes = get_genai_router(ROUTE_PREFIX).await?; - let ui_routes = get_ui_router().await?; // merge all the routes except the auth routes // All routes except the auth, healthcheck, ui and ui settings routes are protect by the auth middleware @@ -75,7 +75,6 @@ pub async fn create_router(app_state: Arc) -> Result { .merge(health_routes) .merge(settings_routes) .merge(auth_routes) - .merge(ui_routes) .route_layer(middleware::from_fn(track_metrics)) .layer(cors) .with_state(app_state)) diff --git a/crates/opsml_server/src/core/ui/mod.rs b/crates/opsml_server/src/core/ui/mod.rs deleted file mode 100644 index 0d20d3ff2..000000000 --- a/crates/opsml_server/src/core/ui/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod route; -pub mod schema; -pub use route::get_ui_router; diff --git a/crates/opsml_server/src/core/ui/route.rs b/crates/opsml_server/src/core/ui/route.rs deleted file mode 100644 index 230b4ec2b..000000000 --- a/crates/opsml_server/src/core/ui/route.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::core::state::AppState; -use anyhow::Result; -use axum::Router; -use axum::{ - http::{header::CONTENT_TYPE, StatusCode, Uri}, - response::{IntoResponse, Response}, - routing::get, -}; -use rust_embed::Embed; -use std::sync::Arc; -use tracing::error; - -#[derive(Embed)] -#[folder = "opsml_ui/site/"] -struct Assets; - -async fn serve_sveltekit_app() -> Response { - match Assets::get("index.html") { - Some(content) => { - let mime = mime_guess::from_path("index.html").first_or_octet_stream(); - ([(CONTENT_TYPE, mime.as_ref())], content.data).into_response() - } - None => not_found().await, - } -} - -async fn get_static_file(path: &str) -> Response { - if path.starts_with("opsml/api/") { - return not_found().await; - } - - if let Some(content) = Assets::get(path) { - let mime = mime_guess::from_path(path).first_or_octet_stream(); - return ([(CONTENT_TYPE, mime.as_ref())], content.data).into_response(); - } - - if is_dynamic_card_route(path) || is_dynamic_genai_card_route(path) { - return serve_sveltekit_app().await; - } - - // Check for root opsml path - if path.starts_with("opsml/") && !path.split('/').next_back().unwrap_or("").contains('.') { - return serve_home_html().await; - } - - if path == "opsml" || path == "opsml/" || path.is_empty() { - return serve_home_html().await; - } - - not_found().await -} - -fn is_dynamic_card_route(path: &str) -> bool { - let parts: Vec<&str> = path.split('/').collect(); - - // /opsml/{registry}/card/{space}/{name}/{version} - parts.len() == 6 - && parts[0] == "opsml" - && parts[2] == "card" - && matches!(parts[1], "data" | "model" | "experiment" | "service") -} - -fn is_dynamic_genai_card_route(path: &str) -> bool { - let parts: Vec<&str> = path.split('/').collect(); - - // /opsml/genai/{subregistry}/card/{space}/{name}/{version} - parts.len() == 7 && parts[0] == "opsml" && parts[1] == "genai" && parts[3] == "card" -} - -async fn static_handler(uri: Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/'); - get_static_file(path).await -} - -async fn serve_home_html() -> Response { - match Assets::get("opsml/home.html") { - Some(content) => { - let mime = mime_guess::from_path("home.html").first_or_octet_stream(); - ([(CONTENT_TYPE, mime.as_ref())], content.data).into_response() - } - None => { - error!("opsml/home.html not found!"); - not_found().await - } - } -} - -async fn not_found() -> Response { - (StatusCode::NOT_FOUND, "404").into_response() -} - -async fn opsml_home() -> impl IntoResponse { - get_static_file("opsml/home.html").await -} - -async fn root_redirect() -> Response { - axum::response::Redirect::permanent("/opsml/home").into_response() -} - -pub async fn get_ui_router() -> Result>> { - Ok(Router::new() - .route("/", get(root_redirect)) - .route("/opsml", get(opsml_home)) - .route("/opsml/home", get(opsml_home)) - .route("/opsml/{*path}", get(static_handler)) - .fallback(static_handler)) -} diff --git a/crates/opsml_server/src/core/ui/schema.rs b/crates/opsml_server/src/core/ui/schema.rs deleted file mode 100644 index f0c4df671..000000000 --- a/crates/opsml_server/src/core/ui/schema.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct CardsRequest { - pub space: Option, -} diff --git a/crates/opsml_server/src/core/user/route.rs b/crates/opsml_server/src/core/user/route.rs index bff1fd212..80cd89f9c 100644 --- a/crates/opsml_server/src/core/user/route.rs +++ b/crates/opsml_server/src/core/user/route.rs @@ -47,7 +47,7 @@ pub async fn create_user( } // Check if user already exists - // This route is only use from creating non-sso users, so auth_type is None + // This route is only used for creating non-sso users, so auth_type is None if let Ok(Some(_)) = state.sql_client.get_user(&create_req.username, None).await { return OpsmlServerError::user_already_exists().into_response(StatusCode::CONFLICT); } diff --git a/crates/opsml_server/src/lib.rs b/crates/opsml_server/src/lib.rs index 65bceded8..f76d97eec 100644 --- a/crates/opsml_server/src/lib.rs +++ b/crates/opsml_server/src/lib.rs @@ -26,7 +26,7 @@ pub async fn start_server() -> Result<(), Box> { let app = create_app().await.unwrap(); // get OPSML_SERVER_PORT from env - let port = std::env::var("OPSML_SERVER_PORT").unwrap_or_else(|_| "3000".to_string()); + let port = std::env::var("OPSML_SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); let addr = format!("0.0.0.0:{port}"); // run it diff --git a/crates/opsml_storage/src/storage/filesystem.rs b/crates/opsml_storage/src/storage/filesystem.rs index 7e088d4ab..0a58246de 100644 --- a/crates/opsml_storage/src/storage/filesystem.rs +++ b/crates/opsml_storage/src/storage/filesystem.rs @@ -350,7 +350,7 @@ mod tests { } pub fn set_env_vars() { - std::env::set_var("OPSML_TRACKING_URI", "http://0.0.0.0:3000"); + std::env::set_var("OPSML_TRACKING_URI", "http://0.0.0.0:8080"); } pub fn unset_env_vars() { diff --git a/docker/official/alpine/Dockerfile b/docker/official/alpine/Dockerfile deleted file mode 100644 index a63dc1ecf..000000000 --- a/docker/official/alpine/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM alpine:latest - -ARG OPSML_SERVER_BINARY - -COPY ${OPSML_SERVER_BINARY} /opsml-server - -CMD ["/opsml-server"] \ No newline at end of file diff --git a/docker/official/debian/Dockerfile b/docker/official/debian/Dockerfile index ca814a1c6..84f905447 100644 --- a/docker/official/debian/Dockerfile +++ b/docker/official/debian/Dockerfile @@ -3,18 +3,44 @@ FROM debian:stable-slim ENV DEBIAN_FRONTEND=noninteractive ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US.UTF-8 +ENV PROJECT_HOME=opsml +# Create non-root user and group +RUN groupadd -r opsml && useradd -r -g opsml -s /bin/bash -d /app opsml + +# Install required packages: nginx, nodejs, netcat, curl, etc. RUN apt-get update --no-install-recommends \ && apt-get install --no-install-recommends --yes \ - ca-certificates tzdata curl \ + ca-certificates tzdata curl nginx netcat-openbsd \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install --no-install-recommends --yes nodejs \ && rm -rf /var/lib/apt/lists/* \ && apt-get autoremove \ && apt-get clean +ARG OPSML_PORT=8000 ARG OPSML_SERVER_BINARY -COPY ${OPSML_SERVER_BINARY} /opsml-server +# Copy built UI, server binary, NGINX config, and entrypoint script +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/node_modules /app/ui/node_modules/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/build /app/ui/build/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/opsml-server /usr/local/bin/opsml-server +COPY docker/official/extras/nginx/nginx.conf.template /etc/nginx/nginx.conf.template +COPY docker/official/extras/entrypoint.sh /entrypoint.sh + +# Set permissions and create directories +RUN mkdir -p /var/log/nginx /var/run \ + && rm -f /etc/nginx/sites-enabled/default \ + && chmod +x /entrypoint.sh \ + && chmod +x /usr/local/bin/opsml-server \ + && chown -R opsml:opsml /var/log/nginx /var/run + +ENV OPSML_SERVER_PORT=${OPSML_SERVER_PORT} + +WORKDIR /app + +USER opsml -RUN chmod +x /opsml-server +EXPOSE ${OPSML_PORT} -CMD ["/opsml-server"] \ No newline at end of file +CMD ["/entrypoint.sh"] \ No newline at end of file diff --git a/docker/official/distroless/Dockerfile b/docker/official/distroless/Dockerfile deleted file mode 100644 index d9fff4492..000000000 --- a/docker/official/distroless/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM debian:bullseye-slim as certs -RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates - -FROM gcr.io/distroless/cc-debian11 -COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - -ARG OPSML_SERVER_BINARY - -COPY ${OPSML_SERVER_BINARY} /opsml-server -USER nonroot:nonroot -ENTRYPOINT ["/opsml-server"] \ No newline at end of file diff --git a/docker/official/extras/entrypoint.sh b/docker/official/extras/entrypoint.sh new file mode 100644 index 000000000..deaeb27ea --- /dev/null +++ b/docker/official/extras/entrypoint.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -e + +# All output goes to stdout/stderr for Kubernetes logging +exec 2>&1 + +# PID tracking +RUST_API_PID="" +SVELTEKIT_PID="" +NGINX_PID="" + +# Cleanup function +cleanup() { + echo "$(date): Received shutdown signal, cleaning up..." + + # Graceful shutdown in reverse order + if [[ -n "$NGINX_PID" ]]; then + echo "$(date): Stopping NGINX..." + kill -QUIT $NGINX_PID 2>/dev/null || true + fi + + if [[ -n "$SVELTEKIT_PID" ]]; then + echo "$(date): Stopping SvelteKit..." + kill -TERM $SVELTEKIT_PID 2>/dev/null || true + fi + + if [[ -n "$RUST_API_PID" ]]; then + echo "$(date): Stopping Rust API..." + kill -TERM $RUST_API_PID 2>/dev/null || true + fi + + # Wait for graceful shutdown + sleep 5 + + # Force kill if still running + for pid in $NGINX_PID $SVELTEKIT_PID $RUST_API_PID; do + if [[ -n "$pid" ]] && kill -0 $pid 2>/dev/null; then + echo "$(date): Force killing process $pid" + kill -KILL $pid 2>/dev/null || true + fi + done + + echo "$(date): Cleanup complete" + exit 0 +} + +# Set up signal handlers for Kubernetes +trap cleanup SIGTERM SIGINT + +# Wait for service function +wait_for_service() { + local port=$1 + local service_name=$2 + local max_attempts=30 + local attempt=1 + + echo "$(date): Waiting for $service_name on port $port..." + + while [ $attempt -le $max_attempts ]; do + if nc -z localhost $port 2>/dev/null; then + echo "$(date): $service_name is ready on port $port" + return 0 + fi + + if [ $((attempt % 5)) -eq 0 ]; then + echo "$(date): Still waiting for $service_name (attempt $attempt/$max_attempts)" + fi + + sleep 2 + attempt=$((attempt + 1)) + done + + echo "$(date): ERROR: $service_name failed to start on port $port after $max_attempts attempts" + return 1 +} + +# update nginx template +envsubst '${OPSML_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + +# Start Rust API server +echo "$(date): Starting Rust API server on port ${OPSML_SERVER_PORT:-8080}..." +/usr/local/bin/opsml-server & +RUST_API_PID=$! +echo "$(date): Rust API PID: $RUST_API_PID" + +# Wait for Rust API +if ! wait_for_service ${OPSML_SERVER_PORT:-8080} "Rust API"; then + echo "$(date): Failed to start Rust API, exiting..." + exit 1 +fi + +# Start SvelteKit server +echo "$(date): Starting SvelteKit server on port 3000..." +cd /app/ui +PORT=3000 NODE_ENV=production node build/index.js & +SVELTEKIT_PID=$! +echo "$(date): SvelteKit PID: $SVELTEKIT_PID" + +# Wait for SvelteKit +if ! wait_for_service 3000 "SvelteKit"; then + echo "$(date): Failed to start SvelteKit, exiting..." + exit 1 +fi + +# Start NGINX +echo "$(date): Testing NGINX configuration..." +if ! nginx -t; then + echo "$(date): NGINX configuration test failed, exiting..." + exit 1 +fi + +echo "$(date): Starting NGINX on port ${OPSML_PORT:-8000}..." +nginx -g "daemon off;" & +NGINX_PID=$! +echo "$(date): NGINX PID: $NGINX_PID" + +# Wait for NGINX +if ! wait_for_service "${OPSML_PORT:-8000}" "NGINX"; then + echo "$(date): Failed to start NGINX, exiting..." + exit 1 +fi + +echo "$(date): All services started successfully" +echo "$(date): Rust API: $RUST_API_PID, SvelteKit: $SVELTEKIT_PID, NGINX: $NGINX_PID" + +# Process monitoring loop +while true; do + # Check if any process has died + for pid_name in "RUST_API_PID:Rust API" "SVELTEKIT_PID:SvelteKit" "NGINX_PID:NGINX"; do + pid_var=$(echo $pid_name | cut -d: -f1) + service_name=$(echo $pid_name | cut -d: -f2) + pid_value=$(eval echo \$$pid_var) + + if [[ -n "$pid_value" ]] && ! kill -0 $pid_value 2>/dev/null; then + echo "$(date): ERROR: $service_name (PID: $pid_value) has died unexpectedly" + cleanup + exit 1 + fi + done + + sleep 10 +done \ No newline at end of file diff --git a/docker/official/extras/nginx/nginx.conf.template b/docker/official/extras/nginx/nginx.conf.template new file mode 100644 index 000000000..8e5b13c1c --- /dev/null +++ b/docker/official/extras/nginx/nginx.conf.template @@ -0,0 +1,117 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log off; # only want to see ui and backend logs + error_log /dev/stderr; + + # Use /tmp for non-root user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + # Performance optimizations + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # Buffer sizes for better performance + client_max_body_size 16M; + client_body_buffer_size 128k; + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_comp_level 6; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + text/javascript + application/wasm; + + + upstream sveltekit_backend { + server 127.0.0.1:3000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + upstream rust_api { + server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + server { + listen ${OPSML_PORT}; + server_name _; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # API routes - route to Rust backend + location /opsml/api/ { + proxy_pass http://rust_api/opsml/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } + + # Health check for Rust API + location /health { + proxy_pass http://rust_api/opsml/api/healthcheck; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + access_log off; # Don't log health checks + } + + # All other routes - route to SvelteKit SSR + location / { + proxy_pass http://sveltekit_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } + } +} \ No newline at end of file diff --git a/docker/official/rocky/Dockerfile b/docker/official/rocky/Dockerfile new file mode 100644 index 000000000..f6b5ad5fb --- /dev/null +++ b/docker/official/rocky/Dockerfile @@ -0,0 +1,40 @@ +FROM rockylinux:9-minimal + +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US.UTF-8 +ENV PROJECT_HOME=opsml + +RUN groupadd -r opsml && useradd -r -g opsml -s /bin/bash -d /app opsml + +RUN microdnf update -y \ + && microdnf install -y \ + ca-certificates tzdata curl nginx nmap-ncat \ + glibc-langpack-en \ + && curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - \ + && microdnf install -y nodejs \ + && microdnf clean all \ + && rm -rf /var/cache/dnf/* + +ARG OPSML_PORT=8000 +ARG OPSML_SERVER_BINARY + +# Copy application files +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/node_modules /app/ui/node_modules/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/build /app/ui/build/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/opsml-server /usr/local/bin/opsml-server +COPY docker/official/extras/nginx/nginx.conf.template /etc/nginx/nginx.conf.template +COPY docker/official/extras/entrypoint.sh /entrypoint.sh + +RUN mkdir -p /var/log/nginx /var/run /var/lib/nginx \ + && rm -f /etc/nginx/conf.d/default.conf \ + && chmod +x /entrypoint.sh \ + && chmod +x /usr/local/bin/opsml-server \ + && chown -R opsml:opsml /var/log/nginx /var/run /var/lib/nginx + +WORKDIR /app + +USER opsml + +EXPOSE ${OPSML_PORT} + +CMD ["/entrypoint.sh"] \ No newline at end of file diff --git a/docker/official/scratch/Dockerfile b/docker/official/scratch/Dockerfile deleted file mode 100644 index 8de338e38..000000000 --- a/docker/official/scratch/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM rust:latest AS builder - -RUN apt update && update-ca-certificates - -FROM scratch - -ARG OPSML_SERVER_BINARY - -COPY ${OPSML_SERVER_BINARY} /opsml-server - -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt - -CMD ["/opsml-server"] \ No newline at end of file diff --git a/docker/official/ubuntu/Dockerfile b/docker/official/ubuntu/Dockerfile index 48b14fce8..57949dc00 100644 --- a/docker/official/ubuntu/Dockerfile +++ b/docker/official/ubuntu/Dockerfile @@ -1,19 +1,41 @@ -FROM ubuntu:latest +FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US.UTF-8 ENV PROJECT_HOME=opsml +RUN groupadd -r opsml && useradd -r -g opsml -s /bin/bash -d /app opsml + RUN apt-get update --no-install-recommends \ && apt-get install --no-install-recommends --yes \ - ca-certificates tzdata curl \ + ca-certificates tzdata curl nginx supervisor netcat-openbsd \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install --no-install-recommends --yes nodejs \ && rm -rf /var/lib/apt/lists/* \ && apt-get autoremove \ && apt-get clean +ARG OPSML_PORT=8000 ARG OPSML_SERVER_BINARY -COPY ${OPSML_SERVER_BINARY} /opsml-server +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/node_modules /app/ui/node_modules/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/ui/build /app/ui/build/ +COPY --chown=opsml:opsml ${OPSML_SERVER_BINARY}/opsml-server /usr/local/bin/opsml-server +COPY docker/official/extras/nginx/nginx.conf.template /etc/nginx/nginx.conf.template +COPY docker/official/extras/entrypoint.sh /entrypoint.sh + +# Set permissions and create directories +RUN mkdir -p /var/log/nginx /var/run \ + && rm -f /etc/nginx/sites-enabled/default \ + && chmod +x /entrypoint.sh \ + && chmod +x /usr/local/bin/opsml-server \ + && chown -R opsml:opsml /var/log/nginx /var/run + +WORKDIR /app + +USER opsml + +EXPOSE ${OPSML_PORT} -CMD ["/opsml-server"] +CMD ["/entrypoint.sh"] \ No newline at end of file diff --git a/py-opsml/docs/docs/evaluation/llm.md b/py-opsml/docs/docs/evaluation/llm.md index 09520787d..08e254bf8 100644 --- a/py-opsml/docs/docs/evaluation/llm.md +++ b/py-opsml/docs/docs/evaluation/llm.md @@ -191,7 +191,7 @@ As you can see from the above example, the overall flow for evaluating an LLM us


- llm metric flow + llm metric flow

diff --git a/py-opsml/docs/docs/setup/overview.md b/py-opsml/docs/docs/setup/overview.md index 95c3d4ba2..17a500c74 100644 --- a/py-opsml/docs/docs/setup/overview.md +++ b/py-opsml/docs/docs/setup/overview.md @@ -25,26 +25,31 @@ Depending on your use case there are a few different ways to setup and run the s ### Docker -The recommended way to run the server is to use Docker. With every release of opsml, we build and publish new Docker images that you can use to run the server. You can find the latest images on [Docker Hub](https://hub.docker.com/r/demml/opsml). +The recommended way to run the server is to use Docker or any other container service. With every release of opsml, we build and publish new container images that you can use to run the server and UI. You can find the latest images on [Docker Hub](https://hub.docker.com/r/demml/opsml). In most cases, you can run the server with the following command; however, you may wish to use our docker images as base images to build your own custom images. ```console -$ docker run -p 3000:3000 demml/opsml:ubuntu-armd64-{version} +$ docker run -p 8080:8000 demml/opsml:ubuntu-armd64-{version} ``` +### What's in the container? + +The server container image is home to both the UI ([sveltekit nodejs app](https://svelte.dev/docs/kit/introduction)) and the opsml server backend ([Axum](https://github.com/tokio-rs/axum)). The UI is always exposed on port `3000` and the server port is exposed on `8080`. NGINX is used as a reverse proxy to route requests to the appropriate service (see `docker/extras` folder for the NGINX configuration) and exposes everything on port `8000` (configurable through `OPSML_PORT`). + +**Note:** The container images are run through an entrypoint script that starts the UI, server, and NGINX services and ensures that they are all running properly. See `docker/official/extras/entrypoint.sh` for more details. The entrypoint does not make use of any process managers like `supervisord` or `systemd` as we prefer the container orchestration system (e.g., Kubernetes etc.) to handle process management. + ### Binary In addition to docker images, we also build and publish new binaries with every release of opsml. These can be download via github and executed directly. - ### Development -As mentioned in the installation section, you can also start the server via the CLI; however, this is not recommended for production use cases as it requires a python runtime to be installed.. The *Docker* and *Binary* methods do not require a python runtime to be installed as they are 100% Rust implementations. +As mentioned in the installation section, you can also start the server via the CLI; however, this is not recommended for production use cases as it requires a python runtime to be installed. We recommend using the prebuilt containers for production use cases as they only require node (pre-installed in the container) to run the UI and the opsml server binary. !!! Note - While the opsml CLI is written in Rust, it is exposed via PyO3 and requires a python runtime to be installed. + While the opsml CLI is written in Rust, it is exposed via PyO3 and requires a python runtime to be installed. Node will also be required to run the UI locally. ```console $ opsml start ui @@ -52,7 +57,7 @@ $ opsml start ui ### Running the Server -opsml supports multiple backends for both the database and storage. By default, opsml will use SQLite for the database and local file storage for the storage backend. You can change this by setting the `OPSML_TRACKING_URI` and `OPSML_STORAGE_URI` environment variables. +opsml supports multiple backends for both the database and storage client. By default, opsml will use SQLite for the database and local file storage for the storage backend. You can change this by setting the `OPSML_TRACKING_URI` and `OPSML_STORAGE_URI` environment variables, respectively. To run the server with a different database backend, you can set the `OPSML_TRACKING_URI` environment variable to the desired backend. For example, to use Postgres, you can set the following environment variable: @@ -109,7 +114,7 @@ Apart from the `OPSML_TRACKING_URI` and `OPSML_STORAGE_URI` environment variable #### OpsML Server Environment Variables - `APP_ENV`: The current environment. This can be set to `development`, `staging` or `production` or anything else you'd want. The default is `development`. -- `OPSML_SERVER_PORT`: The port that the server will run on. The default is `3000`. +- `OPSML_PORT`: The port that the container will run on. The default is `8000`. - `OPSML_ENCRYPT_KEY`: The master encryption key used to encrypt the data at rest. If not set, opsml will use a default **deterministic** key. This is not recommended for production use cases. opsml requires a pbdkdf2::HmacSha256 key with a length of 32 bytes. You can generate a key using the following command with the opsml CLI: ```console @@ -120,7 +125,7 @@ The encryption key (aka jwt_key) is one of the most important pieces to opsml's - `OPSML_REFRESH_SECRET`: The secret used to sign the refresh tokens. This is used to verify the integrity of the refresh tokens. If not set, opsml will use a default **deterministic** key. This is not recommended for production use cases. opsml requires a pbdkdf2::HmacSha256 key with a length of 32 bytes. You can generate a key similar to the `OPSML_ENCRYPT_KEY` key. - `OPSML_MAX_POOL_CONNECTIONS`: The maximum number of connections to the database. The default is `10`. -- `LOG_LEVEL`: The log level for the server. This can be set to `error`, `warn`, `info`, `debug` or `trace`. The default is `info`. +- `LOG_LEVEL`: The log level for the server and UI. This can be set to `error`, `warn`, `info`, `debug` or `trace`. The default is `info`. - `LOG_JSON`: Whether to log in JSON format or not. This can be set to `true` or `false`. The default is `false`. #### Scouter Environment Variables