diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8b9a08f..cd092ba 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -67,7 +67,6 @@ jobs: - name: Svelte check working-directory: frontend run: pnpm check - continue-on-error: true - name: Prettier check working-directory: frontend diff --git a/CHANGELOG.md b/CHANGELOG.md index cf49917..08adce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,75 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [Unreleased] +## [0.6.0] - 2026-10-08 -Make the dashboard follow what each solver can actually do: panels and action buttons are now derived from the solver adapter instead of being shown for every solver. +Make csauto ready for solvers other than code_saturne: one adapter class describes a solver, the CLI and the dashboard read everything from it, and code_aster now runs on the same launch path as code_saturne. Panels and action buttons follow what each solver can actually do. + +### Added +- An adapter conformance suite (`tests/unit/test_adapter_conformance.py`): every registered solver is checked against a sample finished case, so a new adapter learns what the dashboard would miss before a user does. Real runs of the shipped code_saturne and code_aster examples in their docker images are available with `CSAUTO_DOCKER_TESTS=1` (`tests/integration/test_docker_solvers.py`) +- Campaigns record their solver: `csauto prepare` writes `RUNS/campaign.json`, every later command on that folder uses it from any directory, and `prepare` refuses to mix two solvers in one folder. When the `csauto.toml` in use selects another solver, its `docker_image`, `saturne_bin` and `singularity_image` are ignored too (with a warning), since they belong to that other solver +- Runs record their exit status (`.csauto.exitcode`): when the solver's logs give no verdict, exit status 0 means DONE, anything else FAILED. An adapter no longer has to implement `detect_outcome`. Slurm jobs still end with the solver's own status +- Adapter declarations for what used to be code_saturne assumptions: `supported_runtimes`, `container_setup` (shell commands run in the container before the solver), `prepare_launch` (files written before each launch), `restart_modes`, typed `ControlAction` entries, `default_residual_columns`, `logo_file` and `icon_file`, and `read_probe_file` for probe formats other than CSV +- `\{name}` in a template keeps a literal `{name}`, for Python f-strings and sets in code_aster `.comm` files +- API: `/api/tail_files` (the Log Tail file list), `/api/tail_lines` (each line with the severity the solver's anomaly patterns give it), `/api/solver_logo` and `/api/solver_icon`; `/api/app_config` gains `restart_modes`, `default_residual_columns`, `logo` and `icon` ### Changed -- Dashboard panels and action buttons are derived from what the solver adapter implements, rather than declared: a panel appears when the adapter provides what feeds it (`find_residuals_files`, `list_probe_files`, or a non-empty `compare_kinds` / `performance_columns` / `control_actions`), and the Restart, Stop, control and Open GUI controls follow the same rule. Solvers other than code_saturne lose the panels and buttons they could never feed: code_aster and the stub solver now show Status, Compare, Log Tail and Recent Errors only. code_saturne is unchanged. Adapters can no longer declare `dashboard_panels`, which now raises `TypeError` at import time +- `SolverAdapterBase` and the `SolverAdapter` protocol are merged into one base class, `SolverAdapter`. `run_argv` receives the case folder, and the solver starts inside it in every runtime; `gui_argv` receives the setup file's path relative to the case +- Docker runs use `--rm`, so stopped containers no longer pile up. Images keep their own ENTRYPOINT: one that starts the solver (or a script that sets up its environment, then starts it) gets the solver's arguments as before, one that runs the command it is given (`tini --`, `exec "$@"`) gets the full command, and an image without ENTRYPOINT gets the solver through `--entrypoint` (it used to fail). Containers mount the campaign folder at `/csauto` instead of the image user's home, where the solvers wrote `.cache`, `.config` and `.tmp_run_aster` into the campaign folder; a shared folder whose target would land inside that mount point is refused before launch +- `csauto control RUNS CASE ACTION [VALUE]` replaces `--stop`, `--extend N`, `--checkpoint` and `--flush`; the actions come from the solver and `csauto doctor` lists them. `/api/app_config` returns `control_actions` as `{name, label, value_label, value_kind}` objects and `/api/control_case` checks the value against them +- Restart modes come from the solver: `/api/run_case` refuses other names (the `iteration`, `iter`, `time` and `tmax` aliases are gone) and accepts a `restart_path` naming the run to restart from, refusing a run that holds no checkpoint +- Telemetry serve pings report the solver with the runtime (`code_aster:docker` instead of `docker`), so usage of each solver can be told apart; the README lists it among the collected data +- `docker_image` defaults to the solver's own image; it used to default to `simvia/code_saturne` whatever the solver +- A run stays RUNNING until its process or Slurm job ends, even after its log prints a verdict, so its launch slot is not reused and Kill still reaches it +- Clean only deletes the folders the adapter reports as runs (`list_run_dirs`) and never touches RUNNING or PENDING cases, which it now lists (in a dashboard notice, and at the end of `csauto cleanup`, which refreshes statuses first so a run that has ended is cleaned); a finished case returns to PREPARED only when Clean deleted all its runs, not after a logs-only clean or when it had no run to delete +- Cases are found from the registry and their `doe_row.csv` instead of folder names starting with `case`, so custom case ids work with `doctor`, `run`, `serve` and `cleanup` +- `/api/perf` returns the solver's own timing columns +- code_aster: runs `run_aster` through the shared launch path, so the native runtime and Slurm work and the docker command is properly quoted; `n` and `nt` are applied through `.csauto.export`, a copy of the case's export written at each launch (the case's export is never modified, so `csauto prepare` can extend a launched campaign); the verdict is read from `csauto.stdout`; `RESU` holds each case's single run; `MESH` is the only shared folder and is referenced as `../MESH` (the example was updated); the Compare panel offers the export file; code_aster message boxes are flagged in Recent Errors +- The stub solver reads its step count from `stub.toml` instead of misusing `nt` +- The dashboard reads everything solver-specific from the server: the Control menu lists the solver's actions, the Restart dialog its modes and the run to restart from, the Log Tail its log files with severities computed from the solver's patterns, and the header its logo and favicon (now shipped with the adapter in `csauto/solvers/logos/` instead of `frontend/static`). Labels no longer assume code_saturne ("Run folders", "Results (MB)"), and the residual plot preselects the solver's default curves +- Dashboard panels and action buttons are derived from what the solver adapter implements, rather than declared: a panel appears when the adapter provides what feeds it (`find_residuals_files`, `list_probe_files`, or a non-empty `compare_kinds` / `performance_columns` / `control_actions`), and the Restart, Stop, control and Open GUI controls follow the same rule. Solvers other than code_saturne lose the panels and buttons they could never feed: code_aster and the stub solver now show Status, Compare, Log Tail and Recent Errors only. code_saturne is unchanged - `csauto doctor` reports the panels and capabilities derived for the configured solver +- The `info` anomaly label is gone, with the Info choice of the Log Tail and Recent Errors severity filters: no solver produced it, so that filter always came up empty. Adapter patterns are labelled `error` or `warn` +- `/api/probes` takes `scope=probes` instead of `scope=monitoring` (a code_saturne directory name); `profiles` is unchanged. `/api/tail` defaults to the solver's own main log instead of `listing`, and the Log Tail file priority comes from the new adapter attribute `tail_file_names`, exposed as `tail_files` in `/api/app_config` +- code_saturne conventions (log names, outcome, progress and restart patterns, the `RESU` results layout, the `performance.log` parser, setup.xml and run.cfg discovery) moved from `logs.py`, `probes.py`, `residuals.py` and `template.py` into the code_saturne adapter, which now drives the generic log engine with its own vocabulary. CFD anomaly warnings (`divergence`, `cfl`, `clipping`, ...) and the "No error detected" exception now apply to code_saturne only, so Recent Errors no longer flags them for other solvers +- The solver boundary test now scans every module outside `csauto/solvers/` for a wider set of code_saturne conventions (`listing`, `monitoring`, `run_solver.log`, `performance.log`, ...), with the deliberate residue listed in one named allowlist. The leaks it found are gone: the read-only `MESH` mount is the new adapter attribute `readonly_shared_dir_names`, and `csauto tail` defaults to the solver's main log instead of `listing` +- Dashboard tabs are now declared by each solver adapter in `dashboard_panels`, and by nothing else: an adapter may leave out a tab it could feed, and Status, Log Tail and Recent Errors are ordinary entries rather than imposed. A test fails when an adapter declares a tab it cannot feed or an unknown tab name. `capabilities` stays derived. No visible change for any shipped solver + +- The dashboard uses `@lucide/svelte`, the Svelte 5 build of its icon set, instead of `lucide-svelte`. `pnpm check` now reports no errors and gates CI again ### Fixed +- On macOS, a run launched by `csauto serve` no longer stays RUNNING after it ends: the server reaps its own finished processes, since there is no /proc to spot them +- Clean resetting a code_aster case to PREPARED no longer flips back to DONE or FAILED at the next refresh (the reset now clears the start time, so the kept `csauto.stdout` is not read as a verdict) +- code_aster runs with several MPI ranks no longer show run_aster's `If MPI_Abort is called...` notice as an error +- `csauto serve` no longer prints `[telemetry] Sending serve ping` when telemetry is disabled +- An invalid `csauto.toml` (bad TOML, unknown runtime, ...) printed a Python traceback; it now prints a one-line error and exits with status 1 +- code_aster images without `/opt/activate.sh` failed to start; csauto now sources it only when the image has it, so any image with `bash` and `run_aster` on its `PATH` works - A case whose launch failed outright (no docker or `sbatch` on PATH, an unreadable binary, a rejected `sbatch` submission) stayed `PENDING` forever. `_launch_local` and `_launch_slurm` record `status=FAILED` and then raise to report the failure, but `registry_transaction` skipped its save whenever the body raised, so that write was discarded. The save now runs in a `finally`, which also stops `csauto prepare` from discarding the registry entries of cases it already created on disk when it aborts part way - A case launched from the web UI stayed `RUNNING` forever when the run crashed before the solver started (a missing mesh, say). Two causes, both fixed: `is_process_alive` reported a zombie as alive, because `os.kill(pid, 0)` succeeds on a process that exited but was never reaped, which is what every run launched by the long-lived server becomes; and `detect_run_outcome` only read `run_solver.log` and `listing`, so a failure that produces neither went undetected even though code_saturne writes an explicit `run_status.failed` marker next to them. Zombies are now reported as dead (and reaped when we are the parent), and the status markers are read when the logs give no verdict - Opening the solver GUI on a case whose shared dirs are symlinks (the default since `mesh_mode = "symlink"` became the default in 0.4.1) left those symlinks dangling inside the container: `build_gui_command` (docker) and the singularity branch of `build_runtime_gui_command` mounted only the runs dir, unlike their `run` counterparts which also bind the symlink targets. Both now bind them the same way, `MESH` read-only and `POST` writable - Requesting a restart on a solver without restart support returned HTTP 500 "Launch error", a client error reported as a server fault; it now returns HTTP 400 naming the solver - Live control on a solver declaring no control action reported "Invalid action (expected one of [])"; both the API and the CLI now name the solver -- code_aster's Compare panel offered an empty file selector; it now offers `doe_row.csv` +- code_aster's Compare panel offered an empty file selector; it now offers the export file and `doe_row.csv` +- Every run of the shipped code_aster example ended FAILED: its export declares its own message file, so csauto never found the log it read the verdict from +- Kill could stop another campaign's container: for runs without a recorded container (native, apptainer, Slurm, or never started) it searched docker for any container labelled with the case's name, which every campaign shares. It now only searches for docker runs, and only containers of the same campaign +- `sbatch --wrap` submissions requested no resources, so an MPI run got a single task; they now request `--ntasks` and `--cpus-per-task` +- A launch the solver refused (a restart without a checkpoint, an unsupported runtime) left the case PENDING with no way back and skipped the rest of the batch. The case keeps its previous status, the other cases still launch, and the error lists the refused cases +- `csauto run` with a relative runs folder broke docker launches (the container id file path was relative to the wrong folder) +- A PID reused by another process (after a reboot, for example) kept a finished case RUNNING. csauto now records the boot id and the process start time (in clock ticks since boot, which clock steps, DST and time zones leave alone) at launch, and a PID that no longer matches counts as ended, including when another user owns it +- Template rendering silently dropped non-UTF-8 bytes (Latin-1 accents in French comments) and read large binaries in full; files now keep their bytes, and binaries are detected from their first 8 KiB or from a NUL byte further on +- `csauto prepare` on a campaign prepared by csauto 0.5 reported "Existing case differs" for files with CRLF line endings or non-UTF-8 bytes; cases written that way still match +- A `csauto prepare` that failed on one DOE row left that case half made, with unrendered placeholders, which blocked every later prepare; a failing row now leaves nothing behind +- An adapter anomaly pattern with an unknown label made Recent Errors fail with HTTP 500; such labels are now refused when the adapter is defined +- Probe plots ignored a `Time` or `TIME` axis column, and the probe position fell back to the first coordinate row for a probe it could not match +- Residual rows without an `iteration` column were all drawn at x = 0; they are now plotted in row order, with a warning +- The Log Tail never offered `csauto.stdout` and `csauto.stderr`, so it stayed empty for solvers that log to their console and for runs that failed before writing a results folder +- When `/api/app_config` failed once (for example before the API token was entered), the dashboard showed the full code_saturne interface until a reload. It now shows only Status, Log Tail and Recent Errors until the solver's description loads, and fetches it again with a growing delay or as soon as a token is saved +- The Log Tail's case selector disappeared when the selected case had no log yet, leaving the panel stuck on that case +- The Residuals, Probes and Profiles panels hid their case selector, and Probes and Profiles disabled their tab, when the selected case had no results yet, leaving no way to pick another case +- Clean's "Keep specific folders" on several cases deleted every run of all cases but one: the folder list came from a single case. The folder choices are now offered for one case at a time, and Clean leaves alone a case that has none of the folders to keep +- A queued (PENDING) case could flip to DONE or FAILED on the next refresh, from the logs of its previous run +- `csauto tail -n N` printed fewer than N lines when they were long: it only read the last 4 KB of the file +- The file routes (`/api/tail`, `/api/tail_lines`, `/api/case_file`, Compare, Recent Errors) now refuse any file a solver adapter resolves outside the case folder, and a file name with a NUL byte returns 404 instead of HTTP 500. A huge `restart_value` returned HTTP 500 as well ## [0.5.0] - 2026-08-03 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a60b93..857025a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ By participating in this project, you agree to abide by our [Code of Conduct](CO ### Adding a New Solver -Merge requests adding support for new solvers are gladly welcome! csauto's solver-specific logic (commands, log parsing, file conventions, dashboard columns and panels) lives behind a single `SolverAdapter` class, so supporting a new solver means writing one adapter — no changes to the orchestration core or the frontend. Follow the step-by-step guide in [docs/adding-a-solver.md](docs/adding-a-solver.md), and use the built-in `stub` adapter and `tests/unit/test_solver_boundary.py` as references for the expected shape. +Merge requests adding support for new solvers are gladly welcome! csauto knows a solver through one `SolverAdapter` class that says how to start it, where its files are, and how to read its output. Adding a solver means writing that class, registering its name in `csauto/solvers/__init__.py`, and adding a sample finished case to `tests/unit/test_adapter_conformance.py`, which checks the adapter against it. Follow the step-by-step guide in [docs/adding-a-solver.md](docs/adding-a-solver.md); the built-in `stub` adapter (`csauto/solvers/stub.py`) is the smallest complete example. ### Improving Documentation diff --git a/README.md b/README.md index 207c31e..c5d8e96 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

Website - Version + Version CI-CD License

@@ -13,13 +13,13 @@ This project is in early development. Expect breaking changes until v1.0. ## Description -**csauto** automates [code_saturne](https://www.code-saturne.org/) CFD simulation campaigns. +**csauto** automates [code_saturne](https://www.code-saturne.org/) CFD and [code_aster](https://www.code-aster.org/) finite-element simulation campaigns. It offers: - Generation of tens or hundreds of cases from one template + one CSV, in seconds - Local and Slurm job execution with status tracking -- A web dashboard for monitoring residuals, probes, logs, and job status in real time -- Restart from checkpoint, live case steering (stop/extend/checkpoint without killing), input file comparison, and cleanup utilities +- A web dashboard for monitoring job status, logs, and errors in real time, plus residuals, probes, and timings for code_saturne +- Input file comparison and cleanup utilities, plus restart from checkpoint and live case steering (stop/extend/checkpoint without killing) for code_saturne No manual case duplication. No directory juggling. Just simulations. @@ -37,7 +37,7 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/simvia-tech/csauto/main/ source ~/.bashrc # or: source ~/.zshrc ``` -To update an existing installation, re-run the same command — the script detects the existing install, pulls the latest changes, and reinstalls. +To update an existing installation, re-run the same command: the script detects the existing install, pulls the latest changes, and reinstalls.
Manual installation (for contributors or custom locations) @@ -72,13 +72,13 @@ csauto --help ### Prerequisites -You need at least one code_saturne runtime: +You need at least one runtime for your solver: | Runtime | Requirement | |---------------|--------------------------------------------------| -| `native` | `code_saturne` binary in PATH or set via config | +| `native` | The solver's executable (`code_saturne`, or `run_aster` for code_aster) in PATH or set via `saturne_bin` | | `singularity` | Apptainer/Singularity + a `.sif` image | -| `docker` | Docker engine + image | +| `docker` | Docker engine; the image defaults to the solver's own (`simvia/code_saturne`, `simvia/code_aster:17.4.0`) | ## Usage @@ -98,7 +98,7 @@ csauto doctor RUNS csauto serve RUNS --host 127.0.0.1 --port 8000 ``` -Open [http://127.0.0.1:8000](http://127.0.0.1:8000), then launch runs from the **Status** panel: `Ctrl+A` → **Run Selected**. +Open [http://127.0.0.1:8000](http://127.0.0.1:8000), then launch runs from the **Status** panel: `Ctrl+A` → **Run**. See [docs/doe-format.md](docs/doe-format.md) for the `doe.csv` format and the `csauto doe` generator (factorial/LHS/Sobol/CCD). @@ -113,27 +113,30 @@ my-campaign/ │ └── setup.xml ← uses {u_inlet}, {turbulence_model}, etc. └── RUNS/ ← generated by csauto prepare ├── registry.json + ├── campaign.json ← the solver the campaign was prepared for ├── case0001/ └── case0002/ ``` +The solver defaults to code_saturne. For code_aster, set `solver = "code_aster"` in `csauto.toml` and see [examples/codeaster-cube](examples/codeaster-cube/README.md). Later commands on `RUNS` use the solver recorded in `RUNS/campaign.json`, from any directory. + ### CLI commands -The web UI (`csauto serve`) is the primary interface for runtime monitoring and operations — launching, steering, killing, restarting, comparing, and inspecting cases. The CLI covers initial setup (`prepare`, `doctor`), data export (`residuals`, `perf`), and offers terminal alternatives for common actions. +The web UI (`csauto serve`) is the primary interface for runtime monitoring and operations: launching, steering, killing, restarting, comparing, and inspecting cases. The CLI covers initial setup (`prepare`, `doctor`), data export (`residuals`, `perf`), and offers terminal alternatives for common actions. | Command | Description | UI equivalent | |-------------|---------------------------------------------------|----------------------------| -| `prepare` | Generate cases from a DOE CSV and a template | — (CLI only) | -| `doe` | Generate a `doe.csv` from a parameter spec (factorial/LHS/Sobol/CCD) | — (CLI only) | -| `doctor` | Validate runtime environment and configuration | — (CLI only) | -| `serve` | Start the web monitoring dashboard | — (starts the UI) | -| `run` | Launch cases locally or on Slurm (`--n`, `--nt` required) | Status → Run Selected | +| `prepare` | Generate cases from a DOE CSV and a template | CLI only | +| `doe` | Generate a `doe.csv` from a parameter spec (factorial/LHS/Sobol/CCD) | CLI only | +| `doctor` | Validate runtime environment and configuration | CLI only | +| `serve` | Start the web monitoring dashboard | Starts the UI | +| `run` | Launch cases locally or on Slurm (`--n`, `--nt` required) | Status → Run | | `status` | Show case status in the terminal | Status panel | | `tail` | Stream a case log file (like `tail -f`) | Log Tail panel | -| `control` | Steer a running case: stop/extend/checkpoint/flush | Status → Stop / More ▾ menu | +| `control` | Send a live control action to a running case (code_saturne: stop, extend, checkpoint, flush) | Status → Control menu | | `residuals` | Export residuals data and/or SVG plot | Residuals Plot panel | -| `perf` | Extract performance metrics from logs | Timing Snapshot panel | -| `cleanup` | Remove old RESU dirs, logs (`--prune-resu`, etc.) | Status → Clean Selected | +| `perf` | Export the solver's timing metrics | Timing Snapshot panel | +| `cleanup` | Remove old run folders, truncate logs (`--prune-resu`, etc.) | Status → Clean | The web UI also provides features beyond the CLI: kill running cases, edit case files, add notes, mark convergence, compare runs side-by-side, plot probes/profiles, and scan logs for recent errors. @@ -153,6 +156,7 @@ csauto collects anonymous usage statistics to help us understand how the tool is - csauto version - Timezone offset - Runtime type (docker, singularity, or native) +- Solver of the served campaign (code_saturne or code_aster) - Event type (install or serve session) **When it is sent:** @@ -188,12 +192,13 @@ To re-enable: `csauto enable-telemetry` - [HTTP API](docs/api.md) - [Task cookbook](docs/task-cookbook.md) - [Limitations](docs/limitations.md) +- [Adding a new solver](docs/adding-a-solver.md) ## Contributing Please check out our [CONTRIBUTING.md](./CONTRIBUTING.md) file if you want to contribute to the project. -Merge requests adding support for new solvers are gladly welcome: all solver-specific logic lives behind a single adapter class, so a new solver means writing one adapter with no changes to the core or the UI — see [docs/adding-a-solver.md](docs/adding-a-solver.md) for the step-by-step guide. +Merge requests adding support for new solvers are gladly welcome. csauto knows a solver through one adapter class that says how to start it, where its files are, and how to read its output: [docs/adding-a-solver.md](docs/adding-a-solver.md) walks through writing, registering, and testing one. Thank you to all our contributors : @@ -216,6 +221,9 @@ pytest tests/web -q # Run a single test pytest tests/unit/test_foo.py::test_bar -q + +# Real runs of the shipped examples in docker (opt-in, needs the solver images) +CSAUTO_DOCKER_TESTS=1 pytest tests/integration/test_docker_solvers.py -q ``` ### Frontend development @@ -242,7 +250,7 @@ See [architecture](docs/architecture.md) for an overview of the codebase. ## See Also -- [code_saturne](https://www.code-saturne.org/) — the open-source CFD solver csauto automates +- [code_saturne](https://www.code-saturne.org/) and [code_aster](https://www.code-aster.org/): the open-source solvers csauto automates - [Simvia's website](https://simvia.tech) ## License diff --git a/ROADMAP.md b/ROADMAP.md index 7cb72b8..ca7ea6e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,22 +4,22 @@ This roadmap is a declaration of intent, not a contractual engagement. It is updated at each minor or major release. -*Last updated: v0.5.0 — 2026-08-03* +*Last updated: v0.6.0 — 2026-10-08* -## Current Capabilities (v0.5.0) +## Current Capabilities (v0.6.0) - Case generation from DOE CSV + template directory, or a generated parameter spec - Local and Slurm job execution - Web monitoring dashboard (residuals, probes, logs, status) - Restart from checkpoint, live case steering (stop/extend/checkpoint), input file comparison, cleanup - Support for native, Docker, and Singularity runtimes +- code_saturne and code_aster solvers, each described by one adapter class that the CLI and dashboard follow ## Short Term - Add total line count indicator in the log tail panel - Show timing snapshots for previous restarts - Show log tail history for previous restarts -- Avoid copying the mesh folder into the calculation directory to reduce disk space usage ## Medium Term diff --git a/VERSION b/VERSION index 8f0916f..a918a2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/csauto/cli.py b/csauto/cli.py index 6aac9c8..8209e29 100644 --- a/csauto/cli.py +++ b/csauto/cli.py @@ -20,14 +20,15 @@ tail_log, ) from .maintenance import cleanup_runs, run_doctor +from .registry import read_campaign_solver from .residuals import ( collect_residuals, plot_residuals, ) from .runner import refresh_status, run_cases from .serve_commands import add_serve_subcommands, dispatch_serve_command -from .solvers import get_solver_adapter -from .warn import error, flush_warnings +from .solvers import available_solvers, get_solver_adapter +from .warn import error, flush_warnings, warn def serve_fastapi(*args: Any, **kwargs: Any) -> Any: @@ -114,9 +115,8 @@ def parse_arguments( dest="mesh_mode", choices=["copy", "symlink"], default=config.mesh_mode, - help="How to place the solver's shared dirs (meshes, postprocessing) into output_root: 'copy' (default) " - "or 'symlink'. 'symlink' avoids duplicating large meshes but is not supported with the docker/singularity " - "runtimes unless the mesh lives inside output_root already.", + help="How to place the solver's shared dirs (meshes, postprocessing) into output_root: 'symlink' " + "(default, no duplication; containers get the symlink targets mounted) or 'copy'.", ) prepare_parser.add_argument( "--strict", @@ -201,7 +201,7 @@ def parse_arguments( "--docker-image", dest="docker_image", default=config.docker_image, - help="Docker image to use (default from csauto.toml).", + help="Docker image to use (default: docker_image in csauto.toml, else the solver's own image).", ) run_parser.add_argument( "--saturne-bin", @@ -271,7 +271,7 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None: help="Residual columns to plot (e.g. density velocity).", ) - perf_parser = subparsers.add_parser("perf", help="Export performance info from performance.log for selected cases.") + perf_parser = subparsers.add_parser("perf", help="Export the solver's performance metrics for selected cases.") _add_export_args(perf_parser) tail_parser = subparsers.add_parser("tail", help="Follow a case log file (like tail -f).") @@ -280,8 +280,8 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None: tail_parser.add_argument( "--file", dest="file_name", - default="listing", - help="File to follow (listing, run_solver.log, run_status.running, csauto.stdout, ...).", + default=None, + help="File to follow (default: the solver's main log, e.g. csauto.stdout or a path relative to the case).", ) tail_parser.add_argument( "-n", @@ -298,32 +298,14 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None: ) control_parser = subparsers.add_parser( - "control", help="Send a live control directive to a running case (stop/extend/checkpoint/flush)." + "control", + help="Send a live control action to a running case (the actions depend on the solver; " + "csauto doctor lists them).", ) control_parser.add_argument("runs_dir", type=Path, help="Directory containing generated cases") control_parser.add_argument("case", help="Case name (caseXXXX)") - control_action_group = control_parser.add_mutually_exclusive_group(required=True) - control_action_group.add_argument( - "--stop", - action="store_true", - help="Graceful stop: finish the current time step, checkpoint, and exit (no restart needed).", - ) - control_action_group.add_argument( - "--extend", - type=int, - metavar="N", - help="Extend the run by N additional time steps beyond its current progress.", - ) - control_action_group.add_argument( - "--checkpoint", - action="store_true", - help="Request a checkpoint at the next time step.", - ) - control_action_group.add_argument( - "--flush", - action="store_true", - help="Flush logs and time plots at the next time step.", - ) + control_parser.add_argument("action", help="Action name (csauto doctor lists the solver's actions)") + control_parser.add_argument("value", nargs="?", type=float, help="Value, for actions that take one") add_serve_subcommands(subparsers, config) @@ -421,14 +403,38 @@ def _print_doctor(items: Sequence[object]) -> bool: return failed +def _use_campaign_solver(runs_dir: Path | None, config: Config) -> bool: + """Switch `config` to the solver the campaign in `runs_dir` was prepared for. + + Returns True when that replaced the configured solver: the configuration's + solver-specific settings (docker image, solver executable, apptainer image) + then belong to another solver and are dropped. + """ + recorded = read_campaign_solver(runs_dir) if runs_dir is not None else None + if not recorded or recorded == config.solver: + return False + if recorded not in available_solvers(): + raise ValueError(f"{runs_dir}/campaign.json names an unknown solver: {recorded!r}") + if config.path is not None: + warn( + f"{runs_dir} was prepared for {recorded}: ignoring solver, docker_image, saturne_bin " + f"and singularity_image from {config.path}" + ) + config.solver = recorded + config.docker_image = config.saturne_bin = config.singularity_image = None + return True + + def main(argv: Sequence[str] | None = None) -> int: argv_list = list(argv or sys.argv[1:]) - config_path = _preparse_config(argv_list) - config = load_config(config_path) - parser, args = parse_arguments(argv_list, config) - adapter = get_solver_adapter(config.solver) - try: + config = load_config(_preparse_config(argv_list)) + parser, args = parse_arguments(argv_list, config) + # Commands on an existing campaign use the solver it was prepared for, so + # they work from any directory; serve reads it from config.solver. + if _use_campaign_solver(getattr(args, "runs_dir", None), config): + parser, args = parse_arguments(argv_list, config) # drop the other solver's defaults + adapter = get_solver_adapter(config.solver) if args.command is None: parser.print_help() return 0 @@ -527,18 +533,10 @@ def main(argv: Sequence[str] | None = None) -> int: adapter=adapter, ) elif args.command == "control": - if args.stop: - control_action, control_value = "stop", None - elif args.extend is not None: - control_action, control_value = "extend", args.extend - elif args.checkpoint: - control_action, control_value = "checkpoint", None - else: - control_action, control_value = "flush", None details = control_case( - args.runs_dir, args.case, control_action, value=control_value, source="cli", adapter=adapter + args.runs_dir, args.case, args.action, value=args.value, source="cli", adapter=adapter ) - print(f"control: {control_action} -> {details}") + print(f"control: {args.action} -> {details}") elif dispatch_serve_command( args, config, @@ -565,6 +563,7 @@ def main(argv: Sequence[str] | None = None) -> int: if not (args.prune_resu or args.max_log_mb > 0 or args.clear_cid or args.clear_pyc): print("No action specified. Use --prune-resu/--max-log-mb/--clear-cid/--clear-pyc.") return 0 + refresh_status(args.runs_dir, adapter=adapter) # finalize runs that ended, so they are not skipped report = cleanup_runs( args.runs_dir, prune_resu=args.prune_resu, @@ -584,6 +583,8 @@ def main(argv: Sequence[str] | None = None) -> int: print(f"{prefix}.csauto.cid removed: {report.cid_removed}") if args.clear_pyc: print(f"{prefix}__pycache__ removed: {report.pycache_removed}") + if report.skipped_active: + print(f"Skipped running or queued cases: {', '.join(report.skipped_active)}") elif args.command == "completion": from .completion import generate_bash, generate_zsh diff --git a/csauto/config.py b/csauto/config.py index 509a1e8..8e88fd3 100644 --- a/csauto/config.py +++ b/csauto/config.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import Any +from .solvers import DEFAULT_SOLVER + try: import tomllib except ImportError: # pragma: no cover - Python < 3.11 @@ -13,9 +15,9 @@ @dataclass class Config: - solver: str = "code_saturne" + solver: str = DEFAULT_SOLVER runtime: str = "auto" - docker_image: str = "simvia/code_saturne" + docker_image: str | None = None # None: the solver adapter's default image saturne_bin: str | None = None singularity_image: str | None = None singularity_bin: str | None = None @@ -85,7 +87,7 @@ def _parse_optional_bool(value: Any, *, field: str, config_path: Path) -> bool | def _parse_solver(value: Any, *, config_path: Path) -> str: from .solvers import available_solvers - solver = _coerce_str(value, "code_saturne").strip().lower() + solver = _coerce_str(value, DEFAULT_SOLVER).strip().lower() choices = available_solvers() if solver not in choices: raise ValueError(f"Invalid solver in {config_path}: {solver!r} (expected {', '.join(choices)})") @@ -157,7 +159,7 @@ def load_config(path: Path | None = None) -> Config: if "runtime" in data: config.runtime = _parse_runtime(data.get("runtime"), config_path=config_path) if "docker_image" in data: - config.docker_image = _coerce_str(data.get("docker_image"), config.docker_image) + config.docker_image = _coerce_str(data.get("docker_image"), "").strip() or None if "saturne_bin" in data: config.saturne_bin = _coerce_str(data.get("saturne_bin"), "").strip() or None if "singularity_image" in data: diff --git a/csauto/control.py b/csauto/control.py index 484b46f..7682ae5 100644 --- a/csauto/control.py +++ b/csauto/control.py @@ -18,12 +18,12 @@ def control_case( case_id: str, action: str, *, - value: int | None = None, + value: int | float | None = None, source: str = "cli", actor: str | None = None, adapter=None, ) -> dict[str, Any]: - """Send a live control directive (stop/extend/checkpoint/flush) to a running case. + """Send one of the adapter's control actions to a running case. Validates the case is RUNNING and records history; the directive itself is solver-specific and applied by the adapter. @@ -31,8 +31,18 @@ def control_case( adapter = adapter or _default_adapter() if not adapter.control_actions: raise ValueError(f"Solver {adapter.name!r} does not support live control.") - if action not in adapter.control_actions: - raise ValueError(f"Invalid control action: {action!r} (expected one of {sorted(adapter.control_actions)})") + spec = adapter.control_action(action) + if spec is None: + names = ", ".join(a.name for a in adapter.control_actions) + raise ValueError(f"Invalid control action: {action!r} (expected one of: {names})") + if spec.value_label and value is None: + raise ValueError(f"{action} needs a value ({spec.value_label})") + if not spec.value_label and value is not None: + raise ValueError(f"{action} takes no value") + if value is not None and spec.value_kind == "int": + if float(value) != int(value): + raise ValueError(f"{action} needs a whole number ({spec.value_label})") + value = int(value) registry = load_registry(runs_dir) record = registry.get(case_id) diff --git a/csauto/docker.py b/csauto/docker.py index f9f6f5d..790654b 100644 --- a/csauto/docker.py +++ b/csauto/docker.py @@ -1,5 +1,8 @@ from __future__ import annotations +import functools +import hashlib +import json import os import shutil import subprocess @@ -18,80 +21,123 @@ def _default_adapter() -> SolverAdapter: return get_solver_adapter(None) +def campaign_label(runs_root: Path) -> str: + """A short id of a campaign folder, so containers of two campaigns never collide.""" + return hashlib.sha1(str(runs_root.resolve()).encode("utf-8")).hexdigest()[:12] + + +def _docker_prefix(case_dir: Path, adapter: SolverAdapter) -> tuple[list[str], str]: + """`docker run --rm` with the campaign and its shared dirs mounted, plus the in-container case path.""" + from .execution import shared_dir_symlink_mounts + + runs_root = case_dir.parent.resolve() + container_case = f"{adapter.container_root}/{case_dir.name}" + cmd: list[str] = ["docker", "run", "--rm", "-v", f"{runs_root}:{adapter.container_root}"] + for target, readonly in shared_dir_symlink_mounts( + runs_root, adapter.shared_dir_names, adapter.readonly_shared_dir_names + ): + cmd.extend(["-v", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) + display = os.environ.get("DISPLAY") + if display: + cmd.extend(["-e", f"DISPLAY=unix{display}", "-v", "/tmp/.X11-unix:/tmp/.X11-unix"]) + return cmd, container_case + + +ENTRYPOINT_STARTS_SOLVER = "solver" # the image's ENTRYPOINT is the solver, or a script that starts it +ENTRYPOINT_RUNS_COMMAND = "command" # the image's ENTRYPOINT runs the command it is given (tini, exec "$@") + + +def _docker(*args: str, timeout: float) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["docker", *args], capture_output=True, text=True, timeout=timeout, check=False, stdin=subprocess.DEVNULL + ) + + +@functools.cache +def _probe_entrypoint(image_id: str) -> str: + # An entrypoint that runs the command it is given runs `true`; one that starts the solver + # passes `true` to it, which no solver accepts as its arguments. + probe = _docker("run", "--rm", "--network", "none", image_id, "true", timeout=120) + return ENTRYPOINT_RUNS_COMMAND if probe.returncode == 0 else ENTRYPOINT_STARTS_SOLVER + + +def image_entrypoint(docker_image: str, solver_bin: str) -> str | None: + """How the image's own ENTRYPOINT behaves, or None when it has none (or docker cannot tell). + + Site images often set up the solver's environment (modules, spack, conda, + a privilege drop) in their ENTRYPOINT script, so csauto keeps it whenever + the image has one. The probe costs one container per image, once per process. + """ + if not shutil.which("docker"): + return None + try: + inspect = ("image", "inspect", "--format", "{{.Id}} {{json .Config.Entrypoint}}", docker_image) + result = _docker(*inspect, timeout=30) + if result.returncode != 0: + _docker("pull", "-q", docker_image, timeout=1800) # docker run would pull it anyway + result = _docker(*inspect, timeout=30) + if result.returncode != 0: + return None + image_id, _, entrypoint_json = result.stdout.strip().partition(" ") + entrypoint = json.loads(entrypoint_json or "null") + if not entrypoint: + return None + if Path(entrypoint[0]).name == solver_bin: + return ENTRYPOINT_STARTS_SOLVER + return _probe_entrypoint(image_id) + except (OSError, ValueError, subprocess.SubprocessError): + return None + + +def _docker_solver(docker_image: str, args: Sequence[str], adapter: SolverAdapter) -> list[str]: + """Image and solver command, started through the image's own ENTRYPOINT when it has one.""" + from .execution import solver_command + + if not docker_image: + raise ValueError(f"No docker image configured for solver {adapter.name!r} (set docker_image in csauto.toml).") + if not adapter.container_bin_name: + return [docker_image, *args] + entrypoint = image_entrypoint(docker_image, adapter.container_bin_name) + if entrypoint == ENTRYPOINT_STARTS_SOLVER: + return [docker_image, *args] + command = solver_command(adapter, adapter.container_bin_name, args, in_container=True) + if entrypoint == ENTRYPOINT_RUNS_COMMAND: + return [docker_image, *command] + return ["--entrypoint", command[0], docker_image, *command[1:]] + + def build_run_command( case_dir: Path, - nprocs: int, - nt: int, docker_image: str, + args: Sequence[str], cidfile: Path | None = None, - run_args: Sequence[str] | None = None, env_vars: Mapping[str, str] | None = None, adapter: SolverAdapter | None = None, ) -> list[str]: - """Build the docker command to launch a case.""" + """Build the docker command that runs `args` (from run_argv) for a case.""" adapter = adapter or _default_adapter() - display = os.environ.get("DISPLAY") - runs_root = case_dir.parent.resolve() - container_root = adapter.container_root - container_case = f"{container_root}/{case_dir.name}" - from .execution import shared_dir_symlink_mounts - - cmd: list[str] = ["nohup", "docker", "run", "-v", f"{runs_root}:{container_root}"] - for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): - cmd.extend(["-v", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) - if display: - cmd.extend( - [ - "-e", - f"DISPLAY=unix{display}", - "-v", - "/tmp/.X11-unix:/tmp/.X11-unix", - ] - ) + cmd, container_case = _docker_prefix(case_dir, adapter) for key, value in sorted((env_vars or {}).items()): cmd.extend(["-e", f"{key}={value}"]) cmd.extend(["--label", f"csauto.case_id={case_dir.name}"]) + cmd.extend(["--label", f"csauto.campaign={campaign_label(case_dir.parent)}"]) if cidfile: cmd.extend(["--cidfile", str(cidfile)]) - cmd.extend(["-w", container_case, docker_image]) - cmd.extend(adapter.run_argv(container_case, nprocs, nt, run_args)) - return cmd + cmd.extend(["-w", container_case, *_docker_solver(docker_image, args, adapter)]) + return ["nohup", *cmd] def build_gui_command( case_dir: Path, - docker_image: str | None = None, + docker_image: str | None, + args: Sequence[str], adapter: SolverAdapter | None = None, ) -> list[str]: - """Build the docker command to launch the solver GUI for a case.""" + """Build the docker command that opens the solver GUI for a case.""" adapter = adapter or _default_adapter() - docker_image = docker_image or adapter.default_docker_image - display = os.environ.get("DISPLAY") - runs_root = case_dir.parent.resolve() - container_root = adapter.container_root - container_case = f"{container_root}/{case_dir.name}" - setup_path = adapter.find_setup_file(case_dir) - try: - setup_rel = setup_path.relative_to(case_dir) - except ValueError: - setup_rel = Path(setup_path.name) - container_setup = f"{container_case}/{setup_rel.as_posix()}" - from .execution import shared_dir_symlink_mounts - - cmd: list[str] = ["docker", "run", "-v", f"{runs_root}:{container_root}"] - for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): - cmd.extend(["-v", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) - if display: - cmd.extend( - [ - "-e", - f"DISPLAY=unix{display}", - "-v", - "/tmp/.X11-unix:/tmp/.X11-unix", - ] - ) - cmd.extend(["-w", container_case, docker_image]) - cmd.extend(adapter.gui_argv(container_setup)) + cmd, container_case = _docker_prefix(case_dir, adapter) + image = docker_image or adapter.default_docker_image + cmd.extend(["-w", container_case, *_docker_solver(image, args, adapter)]) return cmd @@ -138,45 +184,25 @@ def terminate_container(container_id: str, timeout: int = 10) -> None: ) -def find_container_id_for_case(case_id: str, container_root: str | None = None) -> str | None: - """Best-effort lookup of a running container for a case via docker ps.""" +def find_container_id_for_case(case_id: str, runs_dir: Path) -> str | None: + """Best-effort lookup of the running container of a case, by its csauto labels.""" if not shutil.which("docker"): return None - label_result = subprocess.run( - ["docker", "ps", "-q", "--filter", f"label=csauto.case_id={case_id}"], + result = subprocess.run( + [ + "docker", + "ps", + "-q", + "--filter", + f"label=csauto.case_id={case_id}", + "--filter", + f"label=csauto.campaign={campaign_label(runs_dir)}", + ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, check=False, ) - if label_result.returncode == 0: - for line in label_result.stdout.splitlines(): - cid = line.strip() - if cid: - return cid - if container_root is None: - container_root = _default_adapter().container_root - container_case = f"{container_root}/{case_id}" - ps_result = subprocess.run( - ["docker", "ps", "-q"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - check=False, - ) - if ps_result.returncode != 0: + if result.returncode != 0: return None - for cid in ps_result.stdout.splitlines(): - cid = cid.strip() - if not cid: - continue - inspect = subprocess.run( - ["docker", "inspect", "-f", "{{.Config.WorkingDir}}", cid], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - check=False, - ) - if inspect.returncode == 0 and inspect.stdout.strip() == container_case: - return cid - return None + return next((line.strip() for line in result.stdout.splitlines() if line.strip()), None) diff --git a/csauto/doe.py b/csauto/doe.py index e5774a3..89505ef 100644 --- a/csauto/doe.py +++ b/csauto/doe.py @@ -8,8 +8,16 @@ from pathlib import Path from typing import Any -from .registry import STATUS_PREPARED, registry_transaction, update_case +from .registry import ( + DOE_ROW_FILENAME, + STATUS_PREPARED, + read_campaign_solver, + registry_transaction, + update_case, + write_campaign_solver, +) from .template import ( + ESCAPED_PLACEHOLDER_PATTERN, extract_condition_variables, extract_placeholders, render_template, @@ -29,6 +37,29 @@ def _default_adapter(): DOE_ROW_CACHE: dict[Path, dict[str, Any]] = {} +BINARY_SNIFF_BYTES = 8192 + + +def _read_text(path: Path) -> str: + """Read a template file as UTF-8, keeping any other byte as is (surrogateescape).""" + return path.read_bytes().decode("utf-8", errors="surrogateescape") + + +def _write_text(path: Path, text: str) -> None: + """Write text produced by _read_text, restoring the bytes it kept.""" + path.write_bytes(text.encode("utf-8", errors="surrogateescape")) + + +def _legacy_form(text: str) -> bytes: + """How csauto 0.5 wrote a rendered file: LF line endings, non-UTF-8 bytes dropped.""" + return text.replace("\r\n", "\n").replace("\r", "\n").encode("utf-8", errors="ignore") + + +def _is_binary(path: Path) -> bool: + with path.open("rb") as handle: + return b"\x00" in handle.read(BINARY_SNIFF_BYTES) + + def _is_temp_or_hidden(relative: Path) -> bool: """Return True for editor temp files and hidden paths.""" name = relative.name @@ -92,7 +123,7 @@ def _report_unused_columns(headers: Iterable[str], used_columns: set[str], *, st def _write_doe_row(case_dir: Path, headers: Sequence[str], row: Mapping[str, str], case_id: str) -> None: """Persist the DOE row used for a generated case.""" - doe_path = case_dir / "doe_row.csv" + doe_path = case_dir / DOE_ROW_FILENAME fieldnames = list(headers) if "case_id" not in fieldnames: fieldnames.append("case_id") @@ -163,14 +194,17 @@ def _collect_render_targets(template_dir: Path, adapter) -> list[tuple[Path, str if _is_temp_or_hidden(relative): continue try: - text = path.read_text(encoding="utf-8", errors="ignore") + if _is_binary(path): + continue + text = _read_text(path) except OSError: continue - if "\x00" in text: + if "\x00" in text: # binary past the sniffed head continue placeholders = extract_placeholders(text) cond_vars = extract_condition_variables(text) - if not placeholders and not cond_vars: + # A file whose only braces are escaped still needs rendering, to drop the backslashes. + if not placeholders and not cond_vars and not ESCAPED_PLACEHOLDER_PATTERN.search(text): continue targets.append((relative, text, placeholders, cond_vars)) return targets @@ -225,10 +259,12 @@ def _existing_case_matches( return False if rel_path in rendered_targets: try: - case_text = case_path.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError): + case_text = _read_text(case_path) + except OSError: return False - if case_text != rendered_targets[rel_path]: + if case_text != rendered_targets[rel_path] and _legacy_form(case_text) != _legacy_form( + rendered_targets[rel_path] + ): return False continue try: @@ -268,11 +304,11 @@ def collect_template_variables(template_dir: Path, adapter=None) -> set[str]: adapter = adapter or _default_adapter() if not template_dir.is_dir(): raise FileNotFoundError(f"Template directory not found: {template_dir}") - setup_text = adapter.find_setup_file(template_dir).read_text(encoding="utf-8") + setup_text = _read_text(adapter.find_setup_file(template_dir)) variables = extract_placeholders(setup_text) | extract_condition_variables(setup_text) run_cfg_path = adapter.find_run_config(template_dir) if run_cfg_path: - run_cfg_text = run_cfg_path.read_text(encoding="utf-8") + run_cfg_text = _read_text(run_cfg_path) variables |= extract_placeholders(run_cfg_text) | extract_condition_variables(run_cfg_text) for _, _, extra_placeholders, extra_condition in _collect_render_targets(template_dir, adapter): variables |= extra_placeholders | extra_condition @@ -295,38 +331,53 @@ def generate_cases( raise FileNotFoundError(f"Template directory not found: {template_dir}") adapter = adapter or _default_adapter() + recorded = read_campaign_solver(output_dir) + if recorded and recorded != adapter.name: + raise ValueError(f"{output_dir} holds a {recorded} campaign; csauto.toml selects {adapter.name}.") template_setup = adapter.find_setup_file(template_dir) - setup_text = template_setup.read_text(encoding="utf-8") + setup_text = _read_text(template_setup) placeholders = extract_placeholders(setup_text) condition_vars = extract_condition_variables(setup_text) + variables_by_file: dict[str, set[str]] = {template_setup.name: placeholders | condition_vars} run_cfg_path = adapter.find_run_config(template_dir) run_cfg_template_text: str | None = None run_cfg_placeholders: set[str] = set() run_cfg_condition_vars: set[str] = set() run_cfg_relative: Path | None = None if run_cfg_path: - run_cfg_template_text = run_cfg_path.read_text(encoding="utf-8") + run_cfg_template_text = _read_text(run_cfg_path) run_cfg_relative = run_cfg_path.relative_to(template_dir) run_cfg_placeholders = extract_placeholders(run_cfg_template_text) run_cfg_condition_vars = extract_condition_variables(run_cfg_template_text) placeholders |= run_cfg_placeholders condition_vars |= run_cfg_condition_vars + variables_by_file[run_cfg_relative.as_posix()] = run_cfg_placeholders | run_cfg_condition_vars extra_targets = _collect_render_targets(template_dir, adapter) - for _, _, extra_placeholders, extra_condition in extra_targets: + for rel_path, _, extra_placeholders, extra_condition in extra_targets: placeholders |= extra_placeholders condition_vars |= extra_condition + variables_by_file[rel_path.as_posix()] = extra_placeholders | extra_condition header_set = set(headers) required_columns = placeholders | condition_vars missing_columns = sorted(name for name in required_columns if name not in header_set) if missing_columns: - joined = ", ".join(missing_columns) - raise ValueError(f"Variables without matching DOE columns: {joined}") + where = "; ".join( + f"{name} in {', '.join(sorted(f for f, names in variables_by_file.items() if name in names))}" + for name in missing_columns + ) + example = missing_columns[0] + raise ValueError( + f"Variables without matching DOE columns: {where}. Add the column to the DOE, or write " + f"\\{{{example}}} in the template to keep a literal {{{example}}} (for example in Python code)." + ) _report_unused_columns(headers, required_columns, strict=strict) output_dir.mkdir(parents=True, exist_ok=True) + # Recorded before any case exists, so commands always know the campaign's solver. + write_campaign_solver(output_dir, adapter.name) setup_relative = template_setup.relative_to(template_dir) shared_root = template_dir.parent @@ -360,32 +411,22 @@ def generate_cases( _ensure_registry_case(registry, case_id, case_dir) continue - shutil.copytree(template_dir, case_dir) - rendered = render_template(setup_text, row, case_id) - - target_setup = case_dir / setup_relative - target_setup.write_text(rendered, encoding="utf-8") + # Render everything first: a bad DOE value must not leave a half-made case behind. + rendered_files = {setup_relative: render_template(setup_text, row, case_id)} if run_cfg_template_text and run_cfg_relative: - target_run_cfg = case_dir / run_cfg_relative - target_run_cfg.write_text( - render_template(run_cfg_template_text, row, case_id), - encoding="utf-8", - ) + rendered_files[run_cfg_relative] = render_template(run_cfg_template_text, row, case_id) for rel_path, extra_text, _placeholders_set, _ in extra_targets: - target_path = case_dir / rel_path - if not target_path.exists(): - continue - target_path.write_text( - render_template(extra_text, row, case_id), - encoding="utf-8", - ) + rendered_files[rel_path] = render_template(extra_text, row, case_id) + shutil.copytree(template_dir, case_dir) + for rel_path, text in rendered_files.items(): + _write_text(case_dir / rel_path, text) _write_doe_row(case_dir, headers, row, case_id) _ensure_registry_case(registry, case_id, case_dir) def read_doe_row(case_dir: Path) -> tuple[dict[str, str], list[str]]: """Read the stored DOE row for a case (values and column order).""" - doe_path = case_dir / "doe_row.csv" + doe_path = case_dir / DOE_ROW_FILENAME try: stat = doe_path.stat() except OSError: diff --git a/csauto/execution.py b/csauto/execution.py index 5fd97df..61f5296 100644 --- a/csauto/execution.py +++ b/csauto/execution.py @@ -2,7 +2,7 @@ import os import shutil -from collections.abc import Mapping, Sequence +from collections.abc import Collection, Mapping, Sequence from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -55,7 +55,7 @@ def _resolve_executable(name_or_path: str) -> str | None: return shutil.which(name_or_path) -def resolve_saturne_bin(saturne_bin: str | None, bin_name: str = "code_saturne") -> str | None: +def resolve_saturne_bin(saturne_bin: str | None, bin_name: str) -> str | None: """Resolve the native solver binary, falling back to a PATH lookup of `bin_name`.""" if saturne_bin: resolved = _resolve_executable(saturne_bin) @@ -96,16 +96,25 @@ def resolve_singularity_image(singularity_image: str | None) -> str | None: def resolve_runtime( runtime: str | None, - docker_image: str, + docker_image: str | None = None, saturne_bin: str | None = None, singularity_image: str | None = None, singularity_bin: str | None = None, adapter: SolverAdapter | None = None, ) -> RuntimeSelection: - """Select and validate an execution backend (docker/native/singularity/auto).""" + """Select and validate an execution backend (docker/native/singularity/auto). + + `docker_image` defaults to the adapter's image. Auto mode only considers + the runtimes the adapter supports; asking for another one is an error. + """ adapter = adapter or _default_adapter() selected = normalize_runtime(runtime) bin_name = adapter.native_bin_name + docker_image = docker_image or adapter.default_docker_image + supported = adapter.supported_runtimes + if selected != RUNTIME_AUTO and selected not in supported: + choices = ", ".join(sorted(supported)) + raise ValueError(f"Solver {adapter.name!r} does not support the {selected} runtime (supported: {choices}).") def native_selection(bin_hint: str | None) -> RuntimeSelection: resolved = resolve_saturne_bin(bin_hint, bin_name=bin_name) @@ -143,14 +152,14 @@ def docker_selection() -> RuntimeSelection: if selected == RUNTIME_SINGULARITY: return singularity_selection() - # AUTO mode: - if saturne_bin: + # AUTO mode, among the runtimes the adapter supports: + if saturne_bin and RUNTIME_NATIVE in supported: return native_selection(saturne_bin) - if singularity_image: + if singularity_image and RUNTIME_SINGULARITY in supported: return singularity_selection() - if shutil.which("docker"): + if shutil.which("docker") and RUNTIME_DOCKER in supported and docker_image: return RuntimeSelection(runtime=RUNTIME_DOCKER, docker_image=docker_image) - resolved_native = resolve_saturne_bin(None, bin_name=bin_name) + resolved_native = resolve_saturne_bin(None, bin_name=bin_name) if RUNTIME_NATIVE in supported else None if resolved_native: return RuntimeSelection( runtime=RUNTIME_NATIVE, @@ -158,15 +167,15 @@ def docker_selection() -> RuntimeSelection: saturne_bin=resolved_native, ) raise FileNotFoundError( - f"No execution backend available (docker/{bin_name}/apptainer). " - "Configure runtime/saturne_bin/singularity_image in csauto.toml." + f"No execution backend available for {adapter.name} ({', '.join(sorted(supported))}). " + "Configure runtime/saturne_bin/singularity_image/docker_image in csauto.toml." ) def shared_dir_symlink_mounts( runs_dir: Path, - shared_dirs: Sequence[str] = ("MESH", "POST"), - readonly_dirs: Sequence[str] = ("MESH",), + shared_dirs: Sequence[str], + readonly_dirs: Collection[str] = (), ) -> list[tuple[Path, bool]]: """(target, readonly) pairs for shared dirs that are symlinks out of `runs_dir`. @@ -187,7 +196,9 @@ def shared_dir_symlink_mounts( return mounts -def check_shared_dir_symlinks(runs_dir: Path, runtime: str, shared_dirs: Sequence[str] = ("MESH", "POST")) -> None: +def check_shared_dir_symlinks( + runs_dir: Path, runtime: str, shared_dirs: Sequence[str], container_root: str | None = None +) -> None: """Raise if a symlinked shared dir cannot be made visible inside a container. Symlink targets outside `runs_dir` are bind-mounted into containers at the @@ -197,6 +208,11 @@ def check_shared_dir_symlinks(runs_dir: Path, runtime: str, shared_dirs: Sequenc if runtime not in (RUNTIME_DOCKER, RUNTIME_SINGULARITY): return for target, _readonly in shared_dir_symlink_mounts(runs_dir, shared_dirs): + if container_root and (target == Path(container_root) or Path(container_root) in target.parents): + raise RuntimeError( + f"{target} is mounted at its own path inside the {runtime} container, which collides with " + f"the campaign mount point {container_root}. Move it, or prepare with mesh_mode=copy." + ) if not target.is_dir(): raise RuntimeError( f"{runs_dir} contains a shared-dir symlink to {target}, which does not exist, " @@ -230,6 +246,30 @@ def singularity_shell_exec_prefix(pwd_var: str, env_flags_str: str, extra_binds: return parts +def solver_command(adapter: SolverAdapter, binary: str, args: Sequence[str], *, in_container: bool) -> list[str]: + """The solver's own command, behind the adapter's container setup when it has one.""" + if in_container and adapter.container_setup: + if not binary: + raise ValueError(f"{adapter.name}: container_setup needs a container_bin_name to run after it.") + return ["bash", "-c", f'{adapter.container_setup} && exec "$0" "$@"', binary, *args] + return [binary, *args] + + +def _singularity_prefix(case_dir: Path, selection: RuntimeSelection, adapter: SolverAdapter) -> list[str]: + """`apptainer exec` with the campaign mounted and the case as working directory.""" + if not selection.singularity_bin or not selection.singularity_image: + raise ValueError("Incomplete singularity configuration.") + if not adapter.container_bin_name: + raise ValueError(f"{adapter.name}: the singularity runtime needs a container_bin_name.") + runs_root, container_root, container_case = singularity_paths(case_dir, adapter.container_root) + cmd = [selection.singularity_bin, "exec", "--bind", f"{runs_root}:{container_root}", "--pwd", container_case] + for target, readonly in shared_dir_symlink_mounts( + runs_root, adapter.shared_dir_names, adapter.readonly_shared_dir_names + ): + cmd.extend(["--bind", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) + return cmd + + def build_runtime_run_command( case_dir: Path, nprocs: int, @@ -241,45 +281,34 @@ def build_runtime_run_command( env_vars: Mapping[str, str] | None = None, adapter: SolverAdapter | None = None, ) -> list[str]: - """Build the command list to launch a solver run for the given runtime.""" + """Build the command list to launch a solver run for the given runtime. + + Every runtime starts the command inside the case folder: the runner's + working directory for native runs, `-w` for docker, `--pwd` for apptainer. + """ adapter = adapter or _default_adapter() + args = adapter.run_argv(case_dir, nprocs, nt, run_args) if selection.runtime == RUNTIME_DOCKER: return build_run_command( case_dir, - nprocs, - nt, selection.docker_image, + args, cidfile=cidfile, - run_args=run_args, env_vars=env_vars, adapter=adapter, ) if selection.runtime == RUNTIME_NATIVE: if not selection.saturne_bin: raise ValueError("saturne_bin required for native runtime.") - return ["nohup", selection.saturne_bin, *adapter.run_argv(case_dir, nprocs, nt, run_args)] + return ["nohup", *solver_command(adapter, selection.saturne_bin, args, in_container=False)] if selection.runtime == RUNTIME_SINGULARITY: - if not selection.singularity_bin or not selection.singularity_image: - raise ValueError("Incomplete singularity configuration.") - runs_root, container_root, container_case = singularity_paths(case_dir, adapter.container_root) - cmd = [ - "nohup", - selection.singularity_bin, - "exec", - "--bind", - f"{runs_root}:{container_root}", - "--pwd", - container_case, - ] - for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): - cmd.extend(["--bind", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) + cmd = ["nohup", *_singularity_prefix(case_dir, selection, adapter)] if cleanenv: cmd.append("--cleanenv") for key, value in sorted((env_vars or {}).items()): cmd.extend(["--env", f"{key}={value}"]) - cmd.append(selection.singularity_image) - cmd.append(adapter.container_bin_name) - cmd.extend(adapter.run_argv(container_case, nprocs, nt, run_args)) + cmd.append(str(selection.singularity_image)) + cmd.extend(solver_command(adapter, adapter.container_bin_name, args, in_container=True)) return cmd raise ValueError(f"Unsupported runtime: {selection.runtime}") @@ -289,38 +318,25 @@ def build_runtime_gui_command( selection: RuntimeSelection, adapter: SolverAdapter | None = None, ) -> list[str]: - """Build the command list to open the solver GUI for the given runtime.""" + """Build the command list to open the solver GUI, started inside the case folder.""" adapter = adapter or _default_adapter() - if selection.runtime == RUNTIME_DOCKER: - return build_gui_command(case_dir, docker_image=selection.docker_image, adapter=adapter) setup_path = adapter.find_setup_file(case_dir) try: - setup_rel = setup_path.relative_to(case_dir) + setup_rel = setup_path.relative_to(case_dir).as_posix() except ValueError: - setup_rel = Path(setup_path.name) + setup_rel = setup_path.name + args = adapter.gui_argv(setup_rel) + if selection.runtime == RUNTIME_DOCKER: + return build_gui_command(case_dir, selection.docker_image, args, adapter=adapter) if selection.runtime == RUNTIME_NATIVE: if not selection.saturne_bin: raise ValueError("saturne_bin required for native runtime.") - return [selection.saturne_bin, *adapter.gui_argv(setup_path)] + return solver_command(adapter, selection.saturne_bin, args, in_container=False) if selection.runtime == RUNTIME_SINGULARITY: - if not selection.singularity_bin or not selection.singularity_image: - raise ValueError("Incomplete singularity configuration.") - runs_root, container_root, container_case = singularity_paths(case_dir, adapter.container_root) - container_setup = f"{container_case}/{setup_rel.as_posix()}" - cmd: list[str] = [ - selection.singularity_bin, - "exec", - "--bind", - f"{runs_root}:{container_root}", - "--pwd", - container_case, - ] - for target, readonly in shared_dir_symlink_mounts(runs_root, adapter.shared_dir_names): - cmd.extend(["--bind", f"{target}:{target}:ro" if readonly else f"{target}:{target}"]) + cmd = _singularity_prefix(case_dir, selection, adapter) if os.environ.get("DISPLAY") and Path("/tmp/.X11-unix").exists(): cmd.extend(["--bind", "/tmp/.X11-unix:/tmp/.X11-unix"]) - cmd.append(selection.singularity_image) - cmd.append(adapter.container_bin_name) - cmd.extend(adapter.gui_argv(container_setup)) + cmd.append(str(selection.singularity_image)) + cmd.extend(solver_command(adapter, adapter.container_bin_name, args, in_container=True)) return cmd raise ValueError(f"open_gui not available for runtime {selection.runtime}") diff --git a/csauto/fastapi_app.py b/csauto/fastapi_app.py index 1ff419d..2d71755 100644 --- a/csauto/fastapi_app.py +++ b/csauto/fastapi_app.py @@ -59,7 +59,7 @@ def create_fastapi_app( api_token: str | None = None, solver: str | None = None, runtime: str = "auto", - docker_image: str = "simvia/code_saturne", + docker_image: str | None = None, saturne_bin: str | None = None, singularity_image: str | None = None, singularity_bin: str | None = None, @@ -78,12 +78,13 @@ def create_fastapi_app( status_refresh_lock = threading.Lock() status_cache: dict[str, Any] = {"payload": None, "expires_at": 0.0} + adapter = get_solver_adapter(solver) ctx = FastAPIContext( runs_dir=runs_dir, runs_root=runs_root, api_token=api_token, runtime=runtime, - docker_image=docker_image, + docker_image=docker_image or adapter.default_docker_image, saturne_bin=saturne_bin, singularity_image=singularity_image, singularity_bin=singularity_bin, @@ -94,7 +95,7 @@ def create_fastapi_app( status_cache=status_cache, job_id_patterns=JOB_ID_PATTERNS, http_exception_cls=HTTPException, - adapter=get_solver_adapter(solver), + adapter=adapter, ) build_shared_models(components) @@ -109,7 +110,7 @@ async def _heartbeat_loop() -> None: continue try: uptime = int(_time.monotonic() - start) - await send_event_async(EVENT_SERVE, time_execution=uptime, id_docker=runtime) + await send_event_async(EVENT_SERVE, time_execution=uptime, id_docker=f"{adapter.name}:{runtime}") except Exception: pass @@ -157,7 +158,7 @@ def serve_fastapi( api_token: str | None = None, solver: str | None = None, runtime: str = "auto", - docker_image: str = "simvia/code_saturne", + docker_image: str | None = None, saturne_bin: str | None = None, singularity_image: str | None = None, singularity_bin: str | None = None, diff --git a/csauto/fastapi_routes/actions.py b/csauto/fastapi_routes/actions.py index 7f7d35b..15719fc 100644 --- a/csauto/fastapi_routes/actions.py +++ b/csauto/fastapi_routes/actions.py @@ -7,8 +7,9 @@ from ..control import control_case from ..execution import resolve_runtime +from ..logs import find_case_file from ..maintenance import cleanup_runs -from ..registry import STATUS_DONE, STATUS_FAILED, STATUS_RUNNING, registry_transaction, update_case +from ..registry import STATUS_DONE, STATUS_FAILED, STATUS_PREPARED, STATUS_RUNNING, registry_transaction, update_case from ..runner import run_cases from ..web_support import default_web_max_parallel, kill_case, log_case_action, normalize_convergence from .common import shared_models @@ -53,7 +54,7 @@ class KillCasesPayload(BaseModel): class ControlCasePayload(BaseModel): cases: list[str] | str | None = None action: str | None = None - value: int | None = None + value: float | None = None class OpenGuiPayload(BaseModel): case: str | None = None @@ -65,7 +66,7 @@ class RunCasePayload(BaseModel): max_parallel: int | None = None restart: bool = False restart_mode: str = "" - restart_value: int | float | None = None + restart_value: float | None = None restart_path: str | None = None def normalize_cases(raw: list[str] | str | None) -> list[str] | None: @@ -73,34 +74,38 @@ def normalize_cases(raw: list[str] | str | None) -> list[str] | None: return [raw] return raw + def _option_value(option: Any, raw: float | None, http_exc: type) -> int | float | None: + """Check `raw` against a ControlAction or RestartMode: required, positive, and of its kind.""" + if not option.value_label: + if raw is not None: + raise http_exc(status_code=400, detail=f"{option.name} takes no value") + return None + if raw is None or not math.isfinite(float(raw)) or raw <= 0: + raise http_exc(status_code=400, detail=f"{option.name} needs a positive value ({option.value_label})") + if option.value_kind == "int": + if not float(raw).is_integer(): + raise http_exc(status_code=400, detail=f"{option.name} needs a whole number ({option.value_label})") + return int(raw) + return float(raw) + def _parse_restart_params( payload: RunCasePayload, http_exc: type, ) -> tuple[bool, str | None, int | float | None, str | None]: - restart_mode_raw = (payload.restart_mode or "").strip().lower() - restart_value_raw = payload.restart_value - restart = bool(payload.restart) or bool(restart_mode_raw) or restart_value_raw is not None - restart_path: str | None = None - if payload.restart_path and payload.restart_path.strip(): - restart = True - restart_path = payload.restart_path.strip() + restart_mode_raw = (payload.restart_mode or "").strip() + restart_path = (payload.restart_path or "").strip() or None + restart = bool(payload.restart or restart_mode_raw or payload.restart_value is not None or restart_path) if not restart: return False, None, None, None - restart_mode: str | None = None - restart_value: int | float | None = None - if restart_mode_raw in {"iteration", "iterations", "iter"}: - restart_mode = "iterations" - if restart_value_raw is None or restart_value_raw <= 0 or not float(restart_value_raw).is_integer(): - raise http_exc(status_code=400, detail="restart_value must be a positive integer for iterations") - restart_value = int(restart_value_raw) - elif restart_mode_raw in {"physical_time", "time", "tmax"}: - restart_mode = "physical_time" - if restart_value_raw is None or restart_value_raw <= 0 or not math.isfinite(float(restart_value_raw)): - raise http_exc(status_code=400, detail="restart_value must be a positive number for physical_time") - restart_value = float(restart_value_raw) - elif restart_mode_raw: - raise http_exc(status_code=400, detail="restart_mode must be iterations or physical_time") - return restart, restart_mode, restart_value, restart_path + if not restart_mode_raw: + if payload.restart_value is not None: + raise http_exc(status_code=400, detail="restart_value needs a restart_mode") + return True, None, None, restart_path + mode = ctx.adapter.restart_mode(restart_mode_raw) + if mode is None: + names = ", ".join(m.name for m in ctx.adapter.restart_modes) or "none" + raise http_exc(status_code=400, detail=f"Unknown restart_mode {restart_mode_raw!r} (expected: {names})") + return True, mode.name, _option_value(mode, payload.restart_value, http_exc), restart_path @app.post("/api/case_file", response_model=SuccessResponse) def api_case_file_update( @@ -113,13 +118,9 @@ def api_case_file_update( if not payload.kind or payload.content is None: raise ctx.http_exception_cls(status_code=400, detail="Missing case, kind, content parameters") case_id, case_dir = ctx.validated_case_dir(payload.case) - target = ctx.adapter.locate_case_file(case_dir, payload.kind) + target = find_case_file(case_dir, payload.kind, ctx.adapter) if not target: raise ctx.http_exception_cls(status_code=404, detail=f"File {payload.kind} not found for {case_id}") - try: - target.resolve().relative_to(case_dir.resolve()) - except ValueError as exc: - raise ctx.http_exception_cls(status_code=403, detail="Access denied") from exc try: target.write_text(payload.content, encoding="utf-8") except OSError as exc: @@ -209,6 +210,7 @@ def api_cleanup_cases( if keep_resu and delete_resu: raise ctx.http_exception_cls(status_code=400, detail="keep_resu and delete_resu are mutually exclusive") + had_runs = {case_id for case_id in case_ids if ctx.adapter.list_run_dirs(ctx.runs_dir / case_id)} report = cleanup_runs( ctx.runs_dir, prune_resu=bool(payload.prune_resu), @@ -223,16 +225,24 @@ def api_cleanup_cases( adapter=ctx.adapter, ) actor = request.client.host if request.client else None - # Reset status to PREPARED for cases whose results dir is now empty - if not payload.dry_run: + # A finished case whose runs were all deleted goes back to PREPARED. + if payload.prune_resu and not payload.dry_run: with registry_transaction(ctx.runs_dir) as registry: - for case_id in case_ids: - resu_dir = ctx.adapter.results_root(ctx.runs_dir / case_id) - has_resu = resu_dir.is_dir() and any(resu_dir.iterdir()) - if not has_resu: - current = registry.get(case_id, {}).get("status", "") - if current in (STATUS_DONE, STATUS_FAILED): - update_case(registry, case_id, status="PREPARED", convergence=None) + for case_id in had_runs: + if ctx.adapter.list_run_dirs(ctx.runs_dir / case_id): + continue + current = registry.get(case_id, {}).get("status", "") + if current in (STATUS_DONE, STATUS_FAILED): + # Without a start time, a refresh no longer reads the verdict of the + # deleted run from logs that live outside the run folders. + update_case( + registry, + case_id, + status=STATUS_PREPARED, + convergence=None, + start_time=None, + end_time=None, + ) for case_id in case_ids: log_case_action( ctx.runs_dir, @@ -260,6 +270,7 @@ def api_cleanup_cases( "bytes_freed": report.bytes_freed, "cid_removed": report.cid_removed, "pycache_removed": report.pycache_removed, + "skipped_active": report.skipped_active, } @app.post("/api/kill_case", response_model=SuccessResponse) @@ -320,10 +331,11 @@ def api_control_case( ) -> dict[str, str]: ctx.require_auth(x_csauto_token, authorization) ctx.require_capability("control") - if payload.action not in ctx.adapter.control_actions: - raise ctx.http_exception_cls( - status_code=400, detail=f"Invalid action (expected one of {sorted(ctx.adapter.control_actions)})" - ) + action = ctx.adapter.control_action(payload.action or "") + if action is None: + names = ", ".join(a.name for a in ctx.adapter.control_actions) + raise ctx.http_exception_cls(status_code=400, detail=f"Invalid action (expected one of: {names})") + value = _option_value(action, payload.value, ctx.http_exception_cls) cases = normalize_cases(payload.cases) if not cases: raise ctx.http_exception_cls(status_code=400, detail="Missing cases parameter") @@ -338,8 +350,8 @@ def api_control_case( control_case( ctx.runs_dir, case_ids[0], - payload.action, - value=payload.value, + action.name, + value=value, source="web", actor=actor, adapter=ctx.adapter, @@ -354,8 +366,8 @@ def api_control_case( control_case, ctx.runs_dir, case_id, - payload.action, - value=payload.value, + action.name, + value=value, source="web", actor=actor, adapter=ctx.adapter, @@ -406,7 +418,7 @@ def api_open_gui( try: subprocess.Popen( cmd, - cwd=case_dir.parent, + cwd=case_dir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, diff --git a/csauto/fastapi_routes/case_data.py b/csauto/fastapi_routes/case_data.py index ced5bd8..d4cdc76 100644 --- a/csauto/fastapi_routes/case_data.py +++ b/csauto/fastapi_routes/case_data.py @@ -1,7 +1,13 @@ import math from typing import Annotated, Any -from ..logs import read_tail_lines +from ..logs import ( + GENERIC_ANOMALY_PATTERNS, + find_case_file, + highlight_anomaly_line, + list_tail_files, + read_tail_lines, +) from ..probes import probe_columns, probe_position, render_probe_svg from ..web_support import log_case_action from .common import shared_models @@ -24,16 +30,24 @@ def register_case_data_routes(app: Any, ctx: Any, components: dict[str, Any]) -> class TailQuery(BaseModel): case: str | None = None - file: str = "listing" + file: str | None = None n: int = Field(default=200, ge=1) + class TailLineModel(BaseModel): + text: str + severity: str | None = None + + class TailLinesResponse(BaseModel): + file: str + lines: list[TailLineModel] + class ResuFilesQuery(BaseModel): case: str | None = None limit: int = Field(default=2000, ge=1) class ProbesQuery(BaseModel): case: list[str] | None = None - scope: str = "monitoring" + scope: str = "probes" limit: int = Field(default=200, ge=1) class ProbePositionQuery(BaseModel): @@ -51,6 +65,11 @@ class ProbeSvgQuery(SvgQuery): x_min: float | None = None time_min: float = 0.0 + def _default_tail_file(case_dir: Any) -> str: + """The best log the case has, else the adapter's preferred name (for the 404 message).""" + files = list_tail_files(case_dir, ctx.adapter) if case_dir.is_dir() else [] + return files[0] if files else ctx.adapter.tail_file_names[0] + @app.get("/api/tail", response_model=None) def api_tail( request: Request, @@ -61,18 +80,51 @@ def api_tail( ctx.require_auth(x_csauto_token, authorization) case_id = ctx.validate_case(query.case) case_dir = ctx.runs_dir / case_id - file_path = ctx.adapter.locate_case_file(case_dir, query.file) if case_dir.is_dir() else None + file_name = query.file or _default_tail_file(case_dir) + file_path = find_case_file(case_dir, file_name, ctx.adapter) if case_dir.is_dir() else None if not file_path: - raise ctx.http_exception_cls(status_code=404, detail=f"File {query.file} not found for {case_id}") + raise ctx.http_exception_cls(status_code=404, detail=f"File {file_name} not found for {case_id}") log_case_action( ctx.runs_dir, case_id, "tail", - {"file": query.file, "lines": query.n}, + {"file": file_name, "lines": query.n}, actor=request.client.host if request.client else None, ) return PlainTextResponse("".join(read_tail_lines(file_path, query.n))) + @app.get("/api/tail_files", response_model=StringListResponse) + def api_tail_files( + query: Annotated[CaseQuery, Query()], + x_csauto_token: str | None = Header(default=None), + authorization: str | None = Header(default=None), + ) -> dict[str, Any]: + """Log files the Log Tail panel can show for a case, best first.""" + ctx.require_auth(x_csauto_token, authorization) + _case_id, case_dir = ctx.validated_case_dir(query.case) + return {"files": list_tail_files(case_dir, ctx.adapter)} + + @app.get("/api/tail_lines", response_model=TailLinesResponse) + def api_tail_lines( + query: Annotated[TailQuery, Query()], + x_csauto_token: str | None = Header(default=None), + authorization: str | None = Header(default=None), + ) -> dict[str, Any]: + """Last lines of a case file, each with the severity the solver's anomaly patterns give it.""" + ctx.require_auth(x_csauto_token, authorization) + case_id, case_dir = ctx.validated_case_dir(query.case) + file_name = query.file or _default_tail_file(case_dir) + file_path = find_case_file(case_dir, file_name, ctx.adapter) + if not file_path: + raise ctx.http_exception_cls(status_code=404, detail=f"File {file_name} not found for {case_id}") + patterns = (*GENERIC_ANOMALY_PATTERNS, *ctx.adapter.anomaly_patterns) + lines = [] + for raw in read_tail_lines(file_path, query.n): + text = raw.rstrip("\n") + hit = highlight_anomaly_line(text, patterns, ctx.adapter.anomaly_ignore_patterns) + lines.append({"text": text, "severity": hit[1] if hit else None}) + return {"file": file_name, "lines": lines} + @app.get("/api/resu_files", response_model=StringListResponse) def api_resu_files( query: Annotated[ResuFilesQuery, Query()], @@ -102,8 +154,8 @@ def api_probes( ) -> dict[str, Any]: ctx.require_auth(x_csauto_token, authorization) case_ids = ctx.validate_cases(query.case) - scope_value = (query.scope or "monitoring").strip().lower() - if scope_value not in ("monitoring", "profiles"): + scope_value = query.scope.strip().lower() + if scope_value not in ("probes", "profiles"): raise ctx.http_exception_cls(status_code=400, detail="Invalid scope parameter") seen: set[str] = set() files: list[str] = [] @@ -132,7 +184,7 @@ def api_probe_position( if not query.probe: raise ctx.http_exception_cls(status_code=400, detail="Missing case, probe parameters") _case_id, case_dir = ctx.validated_case_dir(query.case) - position = probe_position(case_dir, query.probe, column_ref=query.column) + position = probe_position(case_dir, query.probe, column_ref=query.column, adapter=ctx.adapter) return {"found": False} if not position else {"found": True, **position} @app.get("/api/probe_columns", response_model=StringListResponse) @@ -150,7 +202,7 @@ def api_probe_columns( seen: set[str] = set() for case_id in case_ids: try: - cols = probe_columns(ctx.runs_dir, case_id, probe_list) + cols = probe_columns(ctx.runs_dir, case_id, probe_list, adapter=ctx.adapter) except FileNotFoundError: continue for c in cols: @@ -195,5 +247,6 @@ def api_probe_svg( x_from=x_value, include_history=query.include_history, restart_values=restart_vals if restart_vals else None, + adapter=ctx.adapter, ) return Response(content=svg, media_type="image/svg+xml") diff --git a/csauto/fastapi_routes/common.py b/csauto/fastapi_routes/common.py index e2a818e..06a99c5 100644 --- a/csauto/fastapi_routes/common.py +++ b/csauto/fastapi_routes/common.py @@ -58,7 +58,7 @@ class SvgQuery(BaseModel): # --- Shared response models --- class SuccessResponse(BaseModel): - """Standard response for successful POST actions.""" + """Standard response for successful actions.""" status: str = "ok" @@ -77,6 +77,7 @@ class CleanupResponse(BaseModel): bytes_freed: int = 0 cid_removed: int = 0 pycache_removed: int = 0 + skipped_active: list[str] = [] class ProbePositionResponse(BaseModel): """Response from probe position lookup.""" diff --git a/csauto/fastapi_routes/compare.py b/csauto/fastapi_routes/compare.py index 1c4c9c0..c453165 100644 --- a/csauto/fastapi_routes/compare.py +++ b/csauto/fastapi_routes/compare.py @@ -3,6 +3,7 @@ from typing import Any from ..diff import compare_runs_diff +from ..logs import find_case_file from ..web_support import log_case_action @@ -23,7 +24,7 @@ def api_case_file( if not kind: raise ctx.http_exception_cls(status_code=400, detail="Missing case, kind parameters") case_id, case_dir = ctx.validated_case_dir(case) - target = ctx.adapter.locate_case_file(case_dir, kind) + target = find_case_file(case_dir, kind, ctx.adapter) if not target: raise ctx.http_exception_cls(status_code=404, detail=f"File {kind} not found for {case_id}") return PlainTextResponse(target.read_text(encoding="utf-8", errors="ignore")) diff --git a/csauto/fastapi_routes/observability.py b/csauto/fastapi_routes/observability.py index d4ffab1..9dee286 100644 --- a/csauto/fastapi_routes/observability.py +++ b/csauto/fastapi_routes/observability.py @@ -1,4 +1,5 @@ import math +from pathlib import Path from typing import Annotated, Any from ..logs import ( @@ -16,6 +17,7 @@ def register_observability_routes(app: Any, ctx: Any, components: dict[str, Any]) -> None: BaseModel = components["BaseModel"] Field = components["Field"] + FileResponse = components["FileResponse"] Header = components["Header"] Query = components["Query"] Request = components["Request"] @@ -58,14 +60,10 @@ class StatusPayloadModel(BaseModel): doe_columns: list[str] class PerformanceRecordModel(BaseModel): + """case_id plus one value per key of the adapter's performance_columns.""" + case_id: str - elapsed_time: str | None = None - mpi_ranks: str | None = None - threads: str | None = None - io_time: str | None = None - linear_solver_time: str | None = None - gradients_time: str | None = None - balances_time: str | None = None + model_config = {"extra": "allow"} class PerfColumnModel(BaseModel): key: str @@ -80,13 +78,26 @@ class CompareKindModel(BaseModel): value: str label: str + class OptionModel(BaseModel): + """A ControlAction or RestartMode: value_label is empty when it takes no value.""" + + name: str + label: str + value_label: str + value_kind: str + class AppConfigModel(BaseModel): solver: str panels: list[str] capabilities: list[str] compare_kinds: list[CompareKindModel] error_files: list[str] - control_actions: list[str] + tail_files: list[str] + control_actions: list[OptionModel] + restart_modes: list[OptionModel] + default_residual_columns: list[str] + logo: bool + icon: bool class RecentErrorItemModel(BaseModel): case_id: str @@ -149,9 +160,29 @@ def api_app_config( "capabilities": sorted(ctx.adapter.capabilities), "compare_kinds": [{"value": kind.value, "label": kind.label} for kind in ctx.adapter.compare_kinds], "error_files": list(ctx.adapter.anomaly_file_names), - "control_actions": sorted(ctx.adapter.control_actions), + "tail_files": list(ctx.adapter.tail_file_names), + "control_actions": [action._asdict() for action in ctx.adapter.control_actions], + "restart_modes": [mode._asdict() for mode in ctx.adapter.restart_modes], + "default_residual_columns": list(ctx.adapter.default_residual_columns), + "logo": bool(ctx.adapter.logo_file and ctx.adapter.logo_file.is_file()), + "icon": bool(ctx.adapter.icon_file and ctx.adapter.icon_file.is_file()), } + def _svg_or_404(path: Path | None) -> Any: + if not path or not path.is_file(): + raise ctx.http_exception_cls(status_code=404, detail=f"No such image for solver {ctx.adapter.name}") + # Revalidate every time: the same URL serves whichever solver this server runs. + return FileResponse(path, media_type="image/svg+xml", headers={"Cache-Control": "no-cache"}) + + # Branding images are public, like the dashboard's own assets. + @app.get("/api/solver_logo", response_model=None) + def api_solver_logo() -> Any: + return _svg_or_404(ctx.adapter.logo_file) + + @app.get("/api/solver_icon", response_model=None) + def api_solver_icon() -> Any: + return _svg_or_404(ctx.adapter.icon_file) + @app.get("/api/restart_origin", response_model=RestartOriginResponse) def api_restart_origin( query: Annotated[CaseListQuery, Query()], diff --git a/csauto/logs.py b/csauto/logs.py index bf69332..5d86cd0 100644 --- a/csauto/logs.py +++ b/csauto/logs.py @@ -14,9 +14,8 @@ from datetime import datetime from pathlib import Path -from .pathutil import is_within_root, safe_subpath +from .pathutil import is_within_root from .registry import STATUS_DONE, STATUS_FAILED -from .template import find_setup_file from .warn import warn @@ -26,8 +25,10 @@ def _default_adapter(): return get_solver_adapter(None) -ANOMALY_FILES_DEFAULT = ("csauto.stderr", "run_solver.log", "listing", "csauto.stdout") -ANOMALY_PATTERNS: list[tuple[str, re.Pattern[str]]] = [ +# Anomaly vocabulary any computation can produce. Adapters extend it with +# their own `anomaly_patterns` and silence false positives with +# `anomaly_ignore_patterns`. +GENERIC_ANOMALY_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( ( "error", re.compile( @@ -37,31 +38,13 @@ def _default_adapter(): re.IGNORECASE, ), ), - ( - "warn", - re.compile( - r"(warning|divergence|unstable|not converged|cfl|clipping|limit reached)", - re.IGNORECASE, - ), - ), -] -ANOMALY_SEVERITY = {"error": 2, "warn": 1, "info": 0} + ("warn", re.compile(r"(warning|limit reached)", re.IGNORECASE)), +) +ANOMALY_SEVERITY = {"error": 2, "warn": 1} ANOMALY_CONTEXT_DEFAULT = 6 ANOMALY_CONTEXT_MAX = 50 ANOMALY_FILE_CACHE_MAX = 256 ANOMALY_FILE_HITS_MAX = 512 -ANOMALY_IGNORE_PATTERNS = [ - re.compile(r"\bno error detected\b", re.IGNORECASE), - re.compile(r"\bno errors detected\b", re.IGNORECASE), -] -RESTART_ITER_PATTERNS = [ - re.compile(r"\bnt_prev\b\s*[:=]\s*(\d+)", re.IGNORECASE), - re.compile(r"NUMBER OF THE PREVIOUS TIME STEP\s+nt_prev\s*=\s*(\d+)", re.IGNORECASE), -] -RESTART_TIME_PATTERNS = [ - re.compile(r"\bt_prev\b\s*[:=]\s*([-+0-9.eE]+)", re.IGNORECASE), - re.compile(r"physical time\s+([-+0-9.eE]+)", re.IGNORECASE), -] ANOMALY_CACHE_LOCK = threading.RLock() ANOMALY_FILE_CACHE: OrderedDict[str, AnomalyFileCache] = OrderedDict() @@ -91,19 +74,23 @@ class AnomalyFileCache: pending_hits: deque[CachedAnomaly] -def highlight_anomaly_line(line: str) -> tuple[str, str] | None: +def highlight_anomaly_line( + line: str, + patterns: Sequence[tuple[str, re.Pattern[str]]] = GENERIC_ANOMALY_PATTERNS, + ignore_patterns: Sequence[re.Pattern[str]] = (), +) -> tuple[str, str] | None: """Highlight anomaly matches in a line and return (html, severity).""" if not line.strip(): return None - for pattern in ANOMALY_IGNORE_PATTERNS: + for pattern in ignore_patterns: if pattern.search(line): return None matches: list[tuple[int, int]] = [] severity: str | None = None - for label, pattern in ANOMALY_PATTERNS: + for label, pattern in patterns: for match in pattern.finditer(line): matches.append((match.start(), match.end())) - if severity is None or ANOMALY_SEVERITY[label] > ANOMALY_SEVERITY.get(severity, -1): + if severity is None or ANOMALY_SEVERITY.get(label, 0) > ANOMALY_SEVERITY.get(severity, -1): severity = label if not matches: return None @@ -122,7 +109,7 @@ def highlight_anomaly_line(line: str) -> tuple[str, str] | None: parts.append(f'{html_lib.escape(line[start:end])}') last = end parts.append(html_lib.escape(line[last:])) - return "".join(parts), severity or "info" + return "".join(parts), severity or "warn" def _new_anomaly_file_cache(path: Path, *, device: int | None, inode: int | None) -> AnomalyFileCache: @@ -179,7 +166,14 @@ def _append_anomaly_hit(cache: AnomalyFileCache, hit: CachedAnomaly) -> None: cache.pending_hits.remove(dropped) -def _scan_anomaly_file(path: Path) -> AnomalyFileCache | None: +# ponytail: the cache is keyed by path only, so hits found with one adapter's +# patterns are reused by another; key it by adapter too if a process ever +# serves several solvers. +def _scan_anomaly_file( + path: Path, + patterns: Sequence[tuple[str, re.Pattern[str]]], + ignore_patterns: Sequence[re.Pattern[str]], +) -> AnomalyFileCache | None: try: stat = path.stat() except OSError: @@ -220,7 +214,7 @@ def _scan_anomaly_file(path: Path) -> AnomalyFileCache | None: still_pending.append(pending) cache.pending_hits = still_pending - highlighted = highlight_anomaly_line(raw_line) + highlighted = highlight_anomaly_line(raw_line, patterns, ignore_patterns) if highlighted: html_line, severity = highlighted _append_anomaly_hit( @@ -259,6 +253,7 @@ def collect_recent_errors( return [] adapter = adapter or _default_adapter() files_to_scan = [f for f in (files or adapter.anomaly_file_names) if f] + patterns = (*GENERIC_ANOMALY_PATTERNS, *adapter.anomaly_patterns) max_hits = max(1, min(max_hits, 500)) context_after = max(0, min(context_after, ANOMALY_CONTEXT_MAX)) allowed = {s.lower() for s in severity_filter} if severity_filter else None @@ -270,7 +265,7 @@ def collect_recent_errors( continue seen_paths: set[str] = set() for name in files_to_scan: - path = adapter.locate_case_file(case_dir, name) + path = find_case_file(case_dir, name, adapter) if not path or not path.is_file(): continue # Aliased names (adapters mapping several conventional names onto @@ -279,7 +274,7 @@ def collect_recent_errors( if resolved_key in seen_paths: continue seen_paths.add(resolved_key) - cache = _scan_anomaly_file(path) + cache = _scan_anomaly_file(path, patterns, adapter.anomaly_ignore_patterns) if cache is None: continue if not cache.hits: @@ -326,131 +321,64 @@ def collect_recent_errors( return results -def locate_case_file(case_dir: Path, name: str) -> Path | None: - """Find a file in a case directory by a friendly name.""" - # Direct path support (relative to case_dir only) - direct = safe_subpath(case_dir, name) - if direct and direct.is_file(): - return direct - - # Common known files with RESU search - if name in {"listing", "run_solver.log", "residuals.csv", "run_status.running", "performance.log"}: - resu_root = case_dir / "RESU" - if resu_root.is_dir(): - resu_dirs = [d for d in resu_root.iterdir() if d.is_dir()] - resu_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) - for resu_dir in resu_dirs: - candidate = resu_dir / name - if candidate.is_file() and is_within_root(candidate, case_dir.resolve()): - return candidate - # Known root-level files - for candidate in [case_dir / "csauto.stdout", case_dir / "csauto.stderr"]: - if candidate.name == name and candidate.is_file(): - return candidate - if name == "setup.xml": - try: - return find_setup_file(case_dir) - except (FileNotFoundError, ValueError): - return None - if name == "doe_row.csv": - candidate = case_dir / "doe_row.csv" - if candidate.is_file(): - return candidate - return None +def list_tail_files(case_dir: Path, adapter=None) -> list[str]: + """Log files of a case for the Log Tail panel, best first, without duplicates. + Each `tail_file_names` entry the adapter resolves comes first, under its + friendly name. Glob entries (such as "*.mess") match files in the case + folder and in the latest run folder. Other *.log files of the latest run + follow. + """ + adapter = adapter or _default_adapter() + files: list[str] = [] + seen: set[Path] = set() -def read_case_file_text(case_dir: Path, name: str, adapter=None) -> str: - """Read a case file content as text.""" + def add(name: str, path: Path) -> None: + resolved = path.resolve() + if path.is_file() and resolved not in seen: + seen.add(resolved) + files.append(name) + + run_dirs = adapter.list_run_dirs(case_dir) + latest = run_dirs[0] if run_dirs else None + older_runs = [run_dir.resolve() for run_dir in run_dirs[1:]] + folders = [case_dir, *([latest] if latest and latest != case_dir else [])] + for name in adapter.tail_file_names: + if any(char in name for char in "*?["): + for folder in folders: + for path in sorted(folder.glob(name)): + add(path.relative_to(case_dir).as_posix(), path) + continue + path = find_case_file(case_dir, name, adapter) + # A previous run's copy would pass for the current run's log while a new run starts. + if path and not any(is_within_root(path, run_dir) for run_dir in older_runs): + add(name, path) + if latest: + for path in sorted(latest.glob("*.log")): + add(path.relative_to(case_dir).as_posix(), path) + return files + + +def find_case_file(case_dir: Path, name: str, adapter=None) -> Path | None: + """adapter.locate_case_file, refused when the file it finds lies outside the case folder. + + Names come from API requests too, so core code goes through here instead of + trusting every adapter override to stay inside the case. + """ adapter = adapter or _default_adapter() path = adapter.locate_case_file(case_dir, name) + return path if path and is_within_root(path, case_dir.resolve()) else None + + +def read_case_file_text(case_dir: Path, name: str, adapter=None) -> str: + """Read a case file content as text.""" + path = find_case_file(case_dir, name, adapter) if not path: raise FileNotFoundError(f"File {name} not found for {case_dir.name}") return path.read_text(encoding="utf-8", errors="ignore") -def extract_restart_origin(case_dir: Path) -> dict[str, int | float]: - """Extract restart origin (iteration/time) from the latest RESU run, if any.""" - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return {} - try: - resu_dirs = [p for p in resu_root.iterdir() if p.is_dir()] - resu_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) - except OSError: - return {} - latest = resu_dirs[0] if resu_dirs else None - if latest is None: - return {} - - iter_value: int | None = None - time_value: float | None = None - for candidate in (latest / "setup.log", latest / "run_solver.log", latest / "listing"): - if not candidate.is_file(): - continue - lines = read_tail_lines(candidate, lines=12000) - if not lines: - continue - for raw in reversed(lines): - if iter_value is None: - for pattern in RESTART_ITER_PATTERNS: - match = pattern.search(raw) - if not match: - continue - try: - iter_value = int(match.group(1)) - except (ValueError, TypeError): - iter_value = None - if iter_value is not None: - break - if time_value is None: - for pattern in RESTART_TIME_PATTERNS: - match = pattern.search(raw) - if not match: - continue - try: - time_value = float(match.group(1)) - except (ValueError, TypeError): - time_value = None - if time_value is not None: - break - if iter_value is not None and time_value is not None: - break - if iter_value is not None and time_value is not None: - break - - result: dict[str, int | float] = {} - if iter_value is not None and iter_value >= 0: - result["iteration"] = iter_value - if time_value is not None and time_value >= 0: - result["time"] = time_value - return result - - -def extract_last_iteration(log_path: Path) -> int | None: - """Extract the last iteration number from a log file, if possible.""" - patterns = [ - re.compile(r"[Ii]teration\s+(\d+)"), - re.compile(r"[Tt]ime\s+step\s+(\d+)"), - re.compile(r"Iter\s*=\s*(\d+)"), - ] - try: - with log_path.open("r", encoding="utf-8", errors="ignore") as handle: - recent_lines = deque(handle, maxlen=400) - except OSError: - return None - - for line in reversed(recent_lines): - for pattern in patterns: - match = pattern.search(line) - if match: - try: - return int(match.group(1)) - except ValueError: - continue - return None - - -def _parse_start_time(start_time: str | None) -> float | None: +def parse_start_time(start_time: str | None) -> float | None: """Parse an ISO start_time into a timestamp (seconds).""" if not start_time: return None @@ -460,7 +388,8 @@ def _parse_start_time(start_time: str | None) -> float | None: return None -def _is_recent(path: Path, start_ts: float | None) -> bool: +def is_recent(path: Path, start_ts: float | None) -> bool: + """True when the file was written at or after the run start (any file counts without a start).""" if start_ts is None: return True try: @@ -469,156 +398,31 @@ def _is_recent(path: Path, start_ts: float | None) -> bool: return False -def locate_log_file(case_dir: Path, start_time: str | None = None) -> Path | None: - """Find a log file to extract iteration information.""" - start_ts = _parse_start_time(start_time) - candidates: list[Path] = [ - case_dir / "listing", - case_dir / "listing.txt", - case_dir / "listing.log", - case_dir / "listing.out", - case_dir / "csauto.stdout", - ] - for name in ("run_solver.log", "listing"): - path = locate_case_file(case_dir, name) - if path: - candidates.append(path) - seen: set[Path] = set() - filtered: list[Path] = [] - for path in candidates: - if path in seen: - continue - seen.add(path) - if path.is_file() and _is_recent(path, start_ts): - filtered.append(path) - if not filtered: - return None - preferred = [ - p for p in filtered if p.name in {"run_solver.log", "listing", "listing.txt", "listing.log", "listing.out"} - ] - if preferred: - return max(preferred, key=lambda p: p.stat().st_mtime) - return max(filtered, key=lambda p: p.stat().st_mtime) - - -def locate_run_status_file(case_dir: Path, start_time: str | None = None) -> Path | None: - """Find the most recent run_status.running file in RESU/*.""" - start_ts = _parse_start_time(start_time) - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return None - candidates: list[Path] = [] - for sub in resu_root.iterdir(): - candidate = sub / "run_status.running" - if candidate.is_file() and _is_recent(candidate, start_ts): - candidates.append(candidate) - if not candidates: - return None - candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True) - return candidates[0] - - -def extract_run_status_iteration(status_path: Path) -> int | None: - """Extract iteration/time step from run_status.running.""" - try: - content = status_path.read_text(encoding="utf-8", errors="ignore") - except OSError: - return None - match = re.search(r"time step:\s*(\d+)", content, re.IGNORECASE) - if match: - try: - return int(match.group(1)) - except ValueError: - return None - return None - - -def detect_run_outcome(case_dir: Path, start_time: str | None = None) -> str | None: - """Determine if the run finished successfully based on solver/listing logs.""" - success_patterns = [ - re.compile(r"END OF CALCULATION", re.IGNORECASE), - re.compile(r"FINAL STAGE OF THE CALCULATION", re.IGNORECASE), - re.compile(r"CALCULATION COMPLETED", re.IGNORECASE), - re.compile(r"Calculation ended normally", re.IGNORECASE), - ] - failure_patterns = [ - re.compile(r"FATAL ERROR", re.IGNORECASE), - re.compile(r"ERROR DETECTED", re.IGNORECASE), - ] - - def has_failure(lines: list[str]) -> bool: - for line in lines: - if any(pattern.search(line) for pattern in ANOMALY_IGNORE_PATTERNS): - continue - if any(pattern.search(line) for pattern in failure_patterns): - return True - return False - - candidates: list[Path] = [] - start_ts = _parse_start_time(start_time) - candidates.extend( - [ - case_dir / "run_solver.log", - case_dir / "listing", - ] - ) - resu_root = case_dir / "RESU" - if resu_root.is_dir(): - resu_dirs = [d for d in resu_root.iterdir() if d.is_dir()] - resu_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) - for resu_dir in resu_dirs: - candidates.extend([resu_dir / "run_solver.log", resu_dir / "listing"]) - - for path in candidates: - if not path.is_file(): +def scan_outcome( + paths: Sequence[Path], + start_time: str | None, + success_patterns: Sequence[re.Pattern[str]], + failure_patterns: Sequence[re.Pattern[str]], + ignore_patterns: Sequence[re.Pattern[str]] = (), + lines: int = 400, +) -> str | None: + """Return STATUS_DONE / STATUS_FAILED from the first log of the current run whose tail gives a verdict.""" + start_ts = parse_start_time(start_time) + for path in paths: + if not path.is_file() or not is_recent(path, start_ts): continue - if not _is_recent(path, start_ts): - continue - lines = read_tail_lines(path, lines=400) - joined = "\n".join(lines) + tail = read_tail_lines(path, lines=lines) + joined = "\n".join(tail) if any(p.search(joined) for p in success_patterns): return STATUS_DONE - if has_failure(lines): - return STATUS_FAILED + for line in tail: + if any(p.search(line) for p in ignore_patterns): + continue + if any(p.search(line) for p in failure_patterns): + return STATUS_FAILED return None -def list_resu_files( - case_dir: Path, - limit: int = 2000, - latest_subdir_only: bool = False, -) -> list[str]: - """Return result files under RESU/ (no deep subfolders), relative to the case directory.""" - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return [] - files: list[str] = [] - if latest_subdir_only: - subdirs = [p for p in resu_root.iterdir() if p.is_dir()] - if not subdirs: - return [] - latest = max(subdirs, key=lambda p: p.stat().st_mtime) - for path in sorted(latest.iterdir()): - if path.is_file(): - files.append(str(path.relative_to(case_dir))) - if len(files) >= limit: - return files - return files - - for path in sorted(resu_root.iterdir()): - if path.is_file(): - files.append(str(path.relative_to(case_dir))) - if len(files) >= limit: - return files - for sub in sorted(p for p in resu_root.iterdir() if p.is_dir()): - for path in sorted(sub.iterdir()): - if path.is_file(): - files.append(str(path.relative_to(case_dir))) - if len(files) >= limit: - return files - return files - - def read_tail_lines(path: Path, lines: int = 20) -> list[str]: """Read the last N lines of a text file.""" try: @@ -631,7 +435,7 @@ def read_tail_lines(path: Path, lines: int = 20) -> list[str]: def tail_log( runs_dir: Path, case: str, - file_name: str, + file_name: str | None, lines: int = 20, follow: bool = True, adapter=None, @@ -642,26 +446,20 @@ def tail_log( raise FileNotFoundError(f"Case not found: {case}") adapter = adapter or _default_adapter() - path = adapter.locate_case_file(case_dir, file_name) + file_name = file_name or next(iter(list_tail_files(case_dir, adapter)), adapter.tail_file_names[0]) + path = find_case_file(case_dir, file_name, adapter) if not path: raise FileNotFoundError(f"File {file_name} not found for {case}") print(f"Tailing {path}") + if lines > 0: + sys.stdout.writelines(read_tail_lines(path, lines)) + sys.stdout.flush() + if not follow: + return with path.open("r", encoding="utf-8", errors="ignore") as handle: + handle.seek(0, os.SEEK_END) try: - handle.seek(0, os.SEEK_END) - file_size = handle.tell() - handle.seek(max(0, file_size - 4096)) - buffer = handle.readlines() - if lines > 0: - buffer = buffer[-lines:] - for line in buffer: - sys.stdout.write(line) - sys.stdout.flush() - - if not follow: - return - while True: where = handle.tell() line = handle.readline() @@ -675,136 +473,6 @@ def tail_log( return -PERFORMANCE_FIELDS = ( - "elapsed_time", - "mpi_ranks", - "threads", - "io_time", - "linear_solver_time", - "gradients_time", - "balances_time", -) - - -def parse_performance_log(path: Path) -> dict[str, str | None]: - """Extract timing and parallel metrics from a performance.log file.""" - try: - content = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - return dict.fromkeys(PERFORMANCE_FIELDS, None) - - value_pattern = r"([0-9.+\-eE]+)" - patterns = { - "elapsed_time": [ - re.compile(rf"(? Path | None: - """Locate the most recent performance.log in RESU subdirectories.""" - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return None - candidates: list[Path] = [] - for resu_dir in resu_root.iterdir(): - if not resu_dir.is_dir(): - continue - candidate = resu_dir / "performance.log" - if candidate.is_file(): - candidates.append(candidate) - if not candidates: - return None - candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True) - return candidates[0] - - def read_performance_rows(runs_dir: Path, cases: Sequence[str], adapter=None) -> list[dict[str, str | None]]: """Read performance metrics for given cases.""" if not cases: @@ -821,7 +489,7 @@ def read_performance_rows(runs_dir: Path, cases: Sequence[str], adapter=None) -> perf_path = adapter.find_performance_log(case_dir) if not perf_path: if adapter.results_root(case_dir).is_dir(): - warn(f"performance.log not found for {case_id}") + warn(f"performance log not found for {case_id}") continue metrics = adapter.parse_performance(perf_path) rec: dict[str, str | None] = {"case_id": case_id} @@ -852,24 +520,17 @@ def collect_performance(runs_dir: Path, cases: Sequence[str], output_path: Path __all__ = [ "ANOMALY_CONTEXT_DEFAULT", - "ANOMALY_FILES_DEFAULT", - "ANOMALY_IGNORE_PATTERNS", - "ANOMALY_PATTERNS", "ANOMALY_SEVERITY", + "GENERIC_ANOMALY_PATTERNS", "collect_performance", "collect_recent_errors", - "detect_run_outcome", - "extract_last_iteration", - "extract_run_status_iteration", - "find_latest_performance_log", "highlight_anomaly_line", - "list_resu_files", - "locate_case_file", - "locate_log_file", - "locate_run_status_file", - "parse_performance_log", + "is_recent", + "list_tail_files", + "parse_start_time", "read_case_file_text", "read_performance_rows", "read_tail_lines", + "scan_outcome", "tail_log", ] diff --git a/csauto/maintenance.py b/csauto/maintenance.py index 5c20943..314bed8 100644 --- a/csauto/maintenance.py +++ b/csauto/maintenance.py @@ -4,10 +4,11 @@ import os import shutil from collections.abc import Iterable, Sequence -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from .pathutil import is_within_root +from .registry import STATUS_PENDING, STATUS_RUNNING, campaign_case_dirs, load_registry @dataclass @@ -69,9 +70,9 @@ def add(level: str, message: str) -> None: else: add("ok", f"write OK: {runs_dir}") - case_dirs = sorted(p for p in runs_dir.iterdir() if p.is_dir() and p.name.startswith("case")) + case_dirs = campaign_case_dirs(runs_dir) if not case_dirs: - add("fail", "no case* directory found") + add("fail", "no case found (run csauto prepare first)") else: add("ok", f"{len(case_dirs)} cases detected") @@ -90,9 +91,16 @@ def add(level: str, message: str) -> None: add("ok", f"solver {adapter.name}: panels {', '.join(adapter.dashboard_panels)}") capability_names = sorted(adapter.capabilities) add("ok", f"solver {adapter.name}: capabilities {', '.join(capability_names) if capability_names else 'none'}") + if adapter.control_actions: + actions = ", ".join(a.name + (f" <{a.value_label}>" if a.value_label else "") for a in adapter.control_actions) + add("ok", f"solver {adapter.name}: control actions {actions}") solver_bin_name = adapter.native_bin_name _rt = (runtime or "auto").strip().lower() + supported = adapter.supported_runtimes + add("ok", f"solver {adapter.name}: runtimes {', '.join(sorted(supported))}") + if _rt != "auto" and _rt not in supported: + add("fail", f"solver {adapter.name} does not support the {_rt} runtime") # Override legacy flags with explicit runtime when provided if runtime: require_docker = _rt == "docker" @@ -146,12 +154,13 @@ def add(level: str, message: str) -> None: _found.append("docker") if shutil.which("apptainer") or shutil.which("singularity"): _found.append("singularity") + _found = [name for name in _found if name in supported] if _found: add("ok", f"available runtimes: {', '.join(_found)}") else: - add("warn", f"no runtime found ({solver_bin_name}, docker, apptainer/singularity)") + add("warn", f"no runtime found for {adapter.name} ({solver_bin_name}, docker, apptainer/singularity)") - if check_display: + if check_display and "gui" in adapter.capabilities: display = os.environ.get("DISPLAY") if not display: add("warn", "DISPLAY not set (GUI unavailable)") @@ -174,6 +183,7 @@ class CleanupReport: bytes_freed: int = 0 cid_removed: int = 0 pycache_removed: int = 0 + skipped_active: list[str] = field(default_factory=list) def _truncate_file(path: Path, max_bytes: int) -> int: @@ -207,7 +217,7 @@ def _safe_case_dir(runs_dir: Path, case_id: str) -> Path | None: def _case_dirs(runs_dir: Path) -> Iterable[Path]: root = runs_dir.resolve() - return sorted(p for p in runs_dir.iterdir() if p.is_dir() and p.name.startswith("case") and is_within_root(p, root)) + return [p for p in campaign_case_dirs(runs_dir) if is_within_root(p, root)] def _iter_case_dirs(runs_dir: Path, cases: Sequence[str] | None) -> Iterable[Path]: @@ -261,14 +271,21 @@ def cleanup_runs( if keep_names and delete_names: raise ValueError("keep_resu and delete_resu are mutually exclusive") log_names = adapter.cleanup_log_names + statuses = {case_id: str(record.get("status") or "") for case_id, record in load_registry(runs_dir).items()} for case_dir in _iter_case_dirs(runs_dir, cases): - resu_root = adapter.results_root(case_dir) - if prune_resu and resu_root.is_dir(): - resu_dirs = [p for p in resu_root.iterdir() if p.is_dir()] - resu_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) + if statuses.get(case_dir.name) in (STATUS_RUNNING, STATUS_PENDING): + # Never delete or truncate files a live run is writing. + report.skipped_active.append(case_dir.name) + continue + # Only the folders the adapter reports as runs are ever deleted. + resu_dirs = adapter.list_run_dirs(case_dir) + if prune_resu: if keep_names: - targets = [resu_dir for resu_dir in resu_dirs if resu_dir.name not in keep_names] + # A case that has none of the kept folders keeps everything: the + # names were picked from another case's runs, or mistyped. + kept = [resu_dir for resu_dir in resu_dirs if resu_dir.name in keep_names] + targets = [resu_dir for resu_dir in resu_dirs if resu_dir.name not in keep_names] if kept else [] elif delete_names: targets = [resu_dir for resu_dir in resu_dirs if resu_dir.name in delete_names] else: @@ -303,12 +320,13 @@ def cleanup_runs( candidate = case_dir / name if candidate.is_file(): candidates.append(candidate) - if resu_root.is_dir(): - for resu_dir in resu_root.iterdir(): - for name in log_names: - candidate = resu_dir / name - if candidate.is_file(): - candidates.append(candidate) + for resu_dir in resu_dirs: + if not resu_dir.is_dir(): + continue + for name in log_names: + candidate = resu_dir / name + if candidate.is_file(): + candidates.append(candidate) for path in candidates: if path.stat().st_size <= max_bytes: continue diff --git a/csauto/pathutil.py b/csauto/pathutil.py index fcdddf2..5ec709b 100644 --- a/csauto/pathutil.py +++ b/csauto/pathutil.py @@ -17,7 +17,10 @@ def safe_subpath(base: Path, relative: str | Path) -> Path | None: rel = Path(relative) if not str(relative) or rel.is_absolute(): return None - candidate = (base / rel).resolve() + try: + candidate = (base / rel).resolve() + except (OSError, ValueError): # a NUL byte, a symlink loop + return None if not is_within_root(candidate, base.resolve()): return None return candidate diff --git a/csauto/probes.py b/csauto/probes.py index e26f70e..6d753b6 100644 --- a/csauto/probes.py +++ b/csauto/probes.py @@ -1,6 +1,7 @@ from __future__ import annotations import csv +import itertools import math from collections.abc import Mapping, Sequence from pathlib import Path @@ -23,36 +24,19 @@ from .warn import warn -def latest_resu_dir(case_dir: Path) -> Path | None: - """Return latest RESU subdirectory by mtime.""" - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return None - subdirs = [d for d in resu_root.iterdir() if d.is_dir()] - if not subdirs: - return None - subdirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) - return subdirs[0] - - -def _iter_resu_subdirs(case_dir: Path) -> list[Path]: - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return [] - return [sub for sub in resu_root.iterdir() if sub.is_dir()] +def _default_adapter(): + from .solvers import get_solver_adapter + return get_solver_adapter(None) -def _iter_resu_subdirs_sorted(case_dir: Path, reverse: bool = False) -> list[Path]: - subdirs = _iter_resu_subdirs(case_dir) - subdirs.sort(key=lambda p: p.stat().st_mtime, reverse=reverse) - return subdirs - -def _list_case_csv_files(case_dir: Path, subdir_name: str, limit: int = 200, with_prefix: bool = False) -> list[str]: - latest = latest_resu_dir(case_dir) - if not latest: +def list_run_csv_files( + run_dir: Path | None, subdir_name: str, limit: int = 200, with_prefix: bool = False +) -> list[str]: + """List CSV files in one subdirectory of a run directory, optionally prefixed with that subdirectory.""" + if run_dir is None: return [] - target_dir = latest / subdir_name + target_dir = run_dir / subdir_name if not target_dir.is_dir(): return [] files: list[str] = [] @@ -65,16 +49,6 @@ def _list_case_csv_files(case_dir: Path, subdir_name: str, limit: int = 200, wit return files -def list_probe_files(case_dir: Path, limit: int = 200) -> list[str]: - """List probe CSV files (by name) in latest RESU monitoring directory.""" - return _list_case_csv_files(case_dir, "monitoring", limit=limit, with_prefix=False) - - -def list_profile_files(case_dir: Path, limit: int = 200) -> list[str]: - """List profile CSV files (with profiles/ prefix) in latest RESU profiles directory.""" - return _list_case_csv_files(case_dir, "profiles", limit=limit, with_prefix=True) - - def _pick_most_recent(paths: Sequence[Path]) -> Path | None: best: Path | None = None best_mtime = float("-inf") @@ -89,97 +63,97 @@ def _pick_most_recent(paths: Sequence[Path]) -> Path | None: return best -def _locate_in_resu_subdir(case_dir: Path, subdir_name: str, name: str) -> Path | None: - latest = latest_resu_dir(case_dir) - latest_candidate: Path | None = None - if latest: - candidate = latest / subdir_name / name - if candidate.is_file() and is_within_root(candidate, case_dir.resolve()): - latest_candidate = candidate - - all_candidates: list[Path] = [] - for sub in _iter_resu_subdirs(case_dir): - candidate = sub / subdir_name / name - if candidate.is_file() and is_within_root(candidate, case_dir.resolve()): - all_candidates.append(candidate) - newest = _pick_most_recent(all_candidates) - if latest_candidate and newest: - # If latest RESU contains the file, keep current behavior and prefer it. - return latest_candidate - return latest_candidate or newest - - -def _locate_in_resu_subdir_all(case_dir: Path, subdir_name: str, name: str) -> list[Path]: +def _run_subdir_matches(case_dir: Path, run_dirs: Sequence[Path], subdir_name: str, name: str) -> list[Path]: + root = case_dir.resolve() matches: list[Path] = [] - for sub in _iter_resu_subdirs_sorted(case_dir, reverse=False): - candidate = sub / subdir_name / name - if candidate.is_file() and is_within_root(candidate, case_dir.resolve()): + for run_dir in run_dirs: + candidate = run_dir / subdir_name / name + if candidate.is_file() and is_within_root(candidate, root): matches.append(candidate) return matches -def locate_probe_files(case_dir: Path, probe_ref: str, include_history: bool = False) -> list[Path]: - """Resolve a probe file reference to one or more file paths. +def _locate_in_run_subdir(case_dir: Path, run_dirs: Sequence[Path], subdir_name: str, name: str) -> Path | None: + matches = _run_subdir_matches(case_dir, run_dirs, subdir_name, name) + # The latest run wins when it has the file, otherwise the most recently written copy. + if matches and matches[0].parent.parent == run_dirs[0]: + return matches[0] + return _pick_most_recent(matches) - If include_history is False, returns at most one file to preserve legacy behavior. - If include_history is True, returns matching files across RESU runs, oldest to newest. + +def locate_run_csv_files( + case_dir: Path, + run_dirs: Sequence[Path], + subdir_names: Sequence[str], + ref: str, + include_history: bool = False, +) -> list[Path]: + """Resolve a probe file reference against subdirectories of the given runs. + + `run_dirs` is newest first. A reference is a path relative to the case, + `/`, or a bare file name searched in `subdir_names` order. + Returns at most one file, or with include_history every match across + runs, oldest to newest. """ - if not probe_ref: + if not ref: return [] - ref_path = Path(probe_ref) + ref_path = Path(ref) direct = safe_subpath(case_dir, ref_path) if direct and direct.is_file(): return [direct] ref_parts = [part for part in ref_path.parts if part not in {".", ""}] - if len(ref_parts) >= 2 and ref_parts[0] in {"monitoring", "profiles"}: - scope = ref_parts[0] + if len(ref_parts) >= 2 and ref_parts[0] in subdir_names: + scopes: Sequence[str] = (ref_parts[0],) name = Path(ref_parts[-1]).name - if include_history: - return _locate_in_resu_subdir_all(case_dir, scope, name) - scoped = _locate_in_resu_subdir(case_dir, scope, name) - return [scoped] if scoped else [] - name = ref_path.name + else: + scopes = subdir_names + name = ref_path.name if not name: return [] - for subdir_name in ("monitoring", "profiles"): + for subdir_name in scopes: if include_history: - matches = _locate_in_resu_subdir_all(case_dir, subdir_name, name) + matches = _run_subdir_matches(case_dir, run_dirs[::-1], subdir_name, name) if matches: return matches else: - candidate = _locate_in_resu_subdir(case_dir, subdir_name, name) + candidate = _locate_in_run_subdir(case_dir, run_dirs, subdir_name, name) if candidate: return [candidate] return [] -def locate_probe_file(case_dir: Path, probe_ref: str) -> Path | None: - """Resolve a probe file reference to a real file path.""" - matches = locate_probe_files(case_dir, probe_ref, include_history=False) - return matches[0] if matches else None +def read_csv_table(path: Path, max_rows: int | None = None) -> tuple[list[str], list[dict[str, str]]]: + """(columns, rows) of a comma-separated CSV file with a header line, keys and values stripped. - -def read_probe_records(case_dir: Path, probe_relpath: str, limit: int = 200) -> tuple[list[str], list[dict[str, str]]]: - """Read probe CSV (limited rows).""" - probe_path = locate_probe_file(case_dir, probe_relpath) - if not probe_path or not probe_path.is_file(): - raise FileNotFoundError(f"Probe file not found: {probe_relpath}") - header: list[str] | None = None - records: list[dict[str, str]] = [] - with probe_path.open(newline="", encoding="utf-8", errors="ignore") as handle: + max_rows stops reading after that many rows (0 reads the header only). + """ + with path.open(newline="", encoding="utf-8", errors="ignore") as handle: reader = csv.DictReader(handle) - if reader.fieldnames: - header = [f.strip() for f in reader.fieldnames if f is not None] - for idx, row in enumerate(reader, start=1): - rec: dict[str, str] = {} - for k, v in row.items(): - if k is None: - continue - rec[k.strip()] = v.strip() if isinstance(v, str) else v - records.append(rec) - if limit and idx >= limit: - break - return header or [], records + if reader.fieldnames is None: + return [], [] + columns = [name.strip() for name in reader.fieldnames if name is not None] + rows = [ + {key.strip(): value.strip() if isinstance(value, str) else value for key, value in row.items() if key} + for row in itertools.islice(reader, max_rows) + ] + return columns, rows + + +def _axis_names(axis: str) -> set[str]: + """Lowercase column names accepted for an x axis: time accepts "t" too.""" + lowered = axis.lower() + if lowered in {"time", "t"}: + return {"time", "t"} + if lowered in {"iteration", "iter"}: + return {"iteration", "iter"} + return {lowered} + + +def locate_probe_file(case_dir: Path, probe_ref: str, adapter=None) -> Path | None: + """Resolve a probe file reference to a real file path.""" + adapter = adapter or _default_adapter() + matches = adapter.locate_probe_files(case_dir, probe_ref, include_history=False) + return matches[0] if matches else None def _normalize_probe_files(probe_files: str | Sequence[str]) -> list[str]: @@ -211,7 +185,20 @@ def _normalize_coord_key(name: str) -> str: "y": {"y", "coordy", "ycoord", "positiony", "yposition", "probey", "y2", "coord2"}, "z": {"z", "coordz", "zcoord", "positionz", "zposition", "probez", "z3", "coord3"}, } -_COORD_INDEX_ALIASES = {"probe", "probeid", "probeindex", "index", "idx", "id", "point", "column", "col"} +_COORD_INDEX_ALIASES = { + "probe", + "probeid", + "probeindex", + "probename", + "name", + "label", + "index", + "idx", + "id", + "point", + "column", + "col", +} def _extract_coord_value(row: Mapping[str, str], axis: str) -> float | None: @@ -259,41 +246,34 @@ def _select_coord_row( try: idx = int(ref) except (ValueError, TypeError): - idx = None - if idx is not None: - if idx > 0 and idx <= len(rows): - return rows[idx - 1] - if idx >= 0 and idx < len(rows): - return rows[idx] - return rows[0] + return None + if 0 < idx <= len(rows): + return rows[idx - 1] + if 0 <= idx < len(rows): + return rows[idx] + return None -def probe_position(case_dir: Path, probe_ref: str, column_ref: str | None = None) -> dict[str, float | str]: - """Return probe coordinates inferred from an associated coords CSV.""" +def probe_position( + case_dir: Path, probe_ref: str, column_ref: str | None = None, adapter=None +) -> dict[str, float | str]: + """Return probe coordinates inferred from an associated coords file, or {} when unknown.""" if not probe_ref: return {} + adapter = adapter or _default_adapter() for candidate in _coords_probe_candidates(probe_ref): - probe_path = locate_probe_file(case_dir, candidate) + probe_path = locate_probe_file(case_dir, candidate, adapter=adapter) if not probe_path or not probe_path.is_file(): continue try: rows_with_coords: list[tuple[Mapping[str, str], float, float, float | None]] = [] - with probe_path.open(newline="", encoding="utf-8", errors="ignore") as handle: - reader = csv.DictReader(handle) - for idx, row in enumerate(reader, start=1): - if idx > 200: - break - if not row: - continue - normalized_row = { - str(k).strip(): v.strip() if isinstance(v, str) else v for k, v in row.items() if k is not None - } - x = _extract_coord_value(normalized_row, "x") - y = _extract_coord_value(normalized_row, "y") - z = _extract_coord_value(normalized_row, "z") - if x is None or y is None: - continue - rows_with_coords.append((normalized_row, x, y, z)) + for row in adapter.read_probe_file(probe_path, max_rows=200)[1]: + x = _extract_coord_value(row, "x") + y = _extract_coord_value(row, "y") + z = _extract_coord_value(row, "z") + if x is None or y is None: + continue + rows_with_coords.append((row, x, y, z)) selected = _select_coord_row(rows_with_coords, column_ref) if selected is not None: _row, x, y, z = selected @@ -315,8 +295,10 @@ def read_probe_rows( axis: str = "time", allow_empty: bool = False, include_history: bool = False, + adapter=None, ) -> tuple[list[str], list[dict[str, str]]]: """Read probe rows for given cases and probe files.""" + adapter = adapter or _default_adapter() if not cases: raise ValueError("At least one case must be specified via --case.") probe_list = _normalize_probe_files(probe_files) @@ -336,35 +318,21 @@ def read_probe_rows( case_status = registry.get(case_id, {}).get("status") case_launched = case_status not in (None, STATUS_PREPARED) for probe_file in probe_list: - probe_paths = locate_probe_files(case_dir, probe_file, include_history=include_history) + probe_paths = adapter.locate_probe_files(case_dir, probe_file, include_history=include_history) if not probe_paths: if case_launched: warn(f"probe not found for {case_id}: {probe_file}") continue for probe_path in probe_paths: - with probe_path.open(newline="", encoding="utf-8", errors="ignore") as handle: - reader = csv.DictReader(handle) - if reader.fieldnames is None: - warn(f"probe has no header for {case_id}") - continue - normalized_fields = [f.strip() for f in reader.fieldnames if f is not None] - if header is None: - header = ["case_id", "probe", *normalized_fields] - else: - if "probe" not in header: - header.append("probe") - for field in normalized_fields: - if field and field not in header: - header.append(field) - for row in reader: - rec = {"case_id": case_id, "probe": probe_file} - for raw_k, raw_v in row.items(): - if raw_k is None: - continue - key = raw_k.strip() - val = raw_v.strip() if isinstance(raw_v, str) else raw_v - rec[key] = val - records.append(rec) + fields, probe_rows = adapter.read_probe_file(probe_path) + if not fields: + warn(f"probe has no header for {case_id}") + continue + if header is None: + header = ["case_id", "probe", *fields] + else: + header.extend(field for field in fields if field and field not in header) + records.extend({"case_id": case_id, "probe": probe_file, **row} for row in probe_rows) if not records and not allow_empty: raise ValueError("No probe data collected.") @@ -375,8 +343,9 @@ def read_probe_rows( return header, records -def probe_columns(runs_dir: Path, case: str, probe_files: str | Sequence[str]) -> list[str]: +def probe_columns(runs_dir: Path, case: str, probe_files: str | Sequence[str], adapter=None) -> list[str]: """Return probe column names (excluding case_id).""" + adapter = adapter or _default_adapter() case_dir = runs_dir / case if not case_dir.is_dir(): raise FileNotFoundError(f"Case not found: {case}") @@ -386,20 +355,13 @@ def probe_columns(runs_dir: Path, case: str, probe_files: str | Sequence[str]) - columns: list[str] = [] seen: set[str] = set() for probe_file in probe_list: - probe_path = locate_probe_file(case_dir, probe_file) + probe_path = locate_probe_file(case_dir, probe_file, adapter=adapter) if not probe_path or not probe_path.is_file(): raise FileNotFoundError(f"Probe file not found: {probe_file}") - with probe_path.open(newline="", encoding="utf-8", errors="ignore") as handle: - reader = csv.DictReader(handle) - if reader.fieldnames is None: - continue - for field in reader.fieldnames: - if field is None: - continue - name = field.strip() - if name and name not in seen: - seen.add(name) - columns.append(name) + for name in adapter.read_probe_file(probe_path, max_rows=0)[0]: + if name and name not in seen: + seen.add(name) + columns.append(name) return columns @@ -414,6 +376,7 @@ def render_probe_svg( x_from: float = 0.0, include_history: bool = False, restart_values: Sequence[float] | None = None, + adapter=None, ) -> str: """Generate an SVG plot for probe data (time or iteration on x-axis).""" probe_list = _normalize_probe_files(probe_file) @@ -425,8 +388,10 @@ def render_probe_svg( axis=axis, allow_empty=True, include_history=include_history, + adapter=adapter, ) - available_cols = [c for c in header if c not in {"case_id", axis, "probe"}] + axis_names = _axis_names(axis) + available_cols = [c for c in header if c not in {"case_id", "probe"} and c.lower() not in axis_names] cols = list(columns) if columns else available_cols[:2] missing = [c for c in cols if c not in available_cols] if missing: @@ -450,22 +415,10 @@ def render_probe_svg( y_min = float("inf") y_max = float("-inf") - ax_lower = axis.lower() - if ax_lower in {"time", "t"}: - axis_candidates_base = ["time", "t"] - elif ax_lower in {"iteration", "iter"}: - axis_candidates_base = ["iteration"] - else: - axis_candidates_base = [axis] - for row in rows: case_id = row.get("case_id", "") probe_name = row.get("probe") if multi_probe else None - x_val = None - for ax_name in axis_candidates_base: - x_val = as_float(row.get(ax_name)) - if x_val is not None: - break + x_val = next((as_float(value) for key, value in row.items() if key.lower() in axis_names), None) if x_val is None: continue if x_val < x_threshold: @@ -549,14 +502,12 @@ def scale_y(y: float) -> float: __all__ = [ - "list_probe_files", - "list_profile_files", + "list_run_csv_files", "locate_probe_file", - "locate_probe_files", + "locate_run_csv_files", "probe_columns", "probe_label_suffix", "probe_position", - "read_probe_records", "read_probe_rows", "render_probe_svg", ] diff --git a/csauto/registry.py b/csauto/registry.py index 85e7442..a226f23 100644 --- a/csauto/registry.py +++ b/csauto/registry.py @@ -22,6 +22,10 @@ REGISTRY_LOCKFILE = f"{REGISTRY_FILENAME}.lock" REGISTRY_THREAD_LOCK = threading.RLock() HISTORY_FILENAME = ".csauto.history.jsonl" +# Records which solver a campaign was prepared for, so commands run from any +# directory use the right adapter. +CAMPAIGN_FILENAME = "campaign.json" +DOE_ROW_FILENAME = "doe_row.csv" try: import fcntl @@ -150,6 +154,29 @@ def timestamp_now() -> str: return datetime.now().isoformat(timespec="seconds") +def campaign_case_dirs(runs_dir: Path) -> list[Path]: + """Case folders of a campaign: those in its registry, plus any folder holding a doe_row.csv.""" + if not runs_dir.is_dir(): + return [] + names = set(load_registry(runs_dir)) | {p.parent.name for p in runs_dir.glob(f"*/{DOE_ROW_FILENAME}")} + return sorted(runs_dir / name for name in names if (runs_dir / name).is_dir()) + + +def read_campaign_solver(runs_dir: Path) -> str | None: + """The solver a campaign was prepared for, or None for campaigns prepared before it was recorded.""" + try: + data = json.loads((runs_dir / CAMPAIGN_FILENAME).read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + solver = data.get("solver") if isinstance(data, dict) else None + return str(solver) if solver else None + + +def write_campaign_solver(runs_dir: Path, solver: str) -> None: + runs_dir.mkdir(parents=True, exist_ok=True) + (runs_dir / CAMPAIGN_FILENAME).write_text(json.dumps({"solver": solver}, indent=2) + "\n", encoding="utf-8") + + def history_path(case_dir: Path) -> Path: """Return the history log path for a case directory.""" return case_dir / HISTORY_FILENAME diff --git a/csauto/residuals.py b/csauto/residuals.py index 42b0275..47a18e8 100644 --- a/csauto/residuals.py +++ b/csauto/residuals.py @@ -2,7 +2,6 @@ import csv import math -import re import sys from collections.abc import Iterator, Sequence from dataclasses import dataclass @@ -41,51 +40,6 @@ class ResidualCaseContext: is_launched: bool # False for PREPARED cases that have never run -def find_latest_residuals(case_dir: Path) -> Path | None: - """Locate the most recent residuals.csv in RESU subdirectories.""" - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return None - candidates: list[Path] = [] - for resu_dir in resu_root.iterdir(): - if not resu_dir.is_dir(): - continue - candidate = resu_dir / "residuals.csv" - if candidate.is_file(): - candidates.append(candidate) - if not candidates: - return None - candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True) - return candidates[0] - - -def find_residuals_files(case_dir: Path, include_history: bool = False) -> list[Path]: - """Locate residuals.csv files for a case. - - If include_history is False, returns at most one file (latest RESU behavior). - If include_history is True, returns all residuals.csv files from RESU, ordered by run age. - """ - latest = find_latest_residuals(case_dir) - if not include_history: - return [latest] if latest else [] - resu_root = case_dir / "RESU" - if not resu_root.is_dir(): - return [] - candidates: list[Path] = [] - for resu_dir in resu_root.iterdir(): - if not resu_dir.is_dir(): - continue - candidate = resu_dir / "residuals.csv" - if candidate.is_file(): - candidates.append(candidate) - candidates.sort(key=lambda p: (p.parent.stat().st_mtime, p.stat().st_mtime)) - return candidates - - -def _looks_like_number(token: str) -> bool: - return as_float(token) is not None - - def _normalize_residual_row(row: dict[str | None, str | None]) -> dict[str, str]: normalized: dict[str, str] = {} for raw_k, raw_v in row.items(): @@ -171,7 +125,7 @@ def _collect_residual_header_from_contexts( for residual_path in context.residual_paths: fields, _rows, _max_iter = _read_residual_csv(residual_path) if fields is None: - warn(f"residuals.csv has no header for {context.case_id}") + warn(f"residuals file has no header for {context.case_id}") continue header = _merge_header_fields(header, fields) added_for_case += 1 @@ -180,7 +134,7 @@ def _collect_residual_header_from_contexts( if fields and rows_local: header = _merge_header_fields(header, fields) elif added_for_case == 0 and context.is_launched: - warn(f"residuals.csv not found for {context.case_id}") + warn(f"residuals not found for {context.case_id}") return header @@ -232,65 +186,6 @@ def _prepare_residual_contexts( return contexts, header -def parse_residuals_from_log(log_path: Path) -> tuple[list[str], list[dict[str, str]]]: - """Parse residual blocks from run_solver.log.""" - if not log_path or not log_path.is_file(): - return [], [] - block_idx = 0 - current: dict[str, str] | None = None - block_has_data = False - rows: list[dict[str, str]] = [] - fields: list[str] = ["iteration"] - header_re = re.compile(r"Variable\s+Rhs norm", re.IGNORECASE) - sep_re = re.compile(r"^-{3,}") - try: - with log_path.open("r", encoding="utf-8", errors="ignore") as handle: - for line in handle: - if header_re.search(line): - if current: - rows.append(current) - current = None - block_has_data = False - block_idx += 1 - current = {"iteration": str(block_idx)} - continue - if current is not None: - if sep_re.match(line) and not block_has_data: - continue - if not line.strip() or sep_re.match(line): - if current: - rows.append(current) - current = None - block_has_data = False - continue - parts = line.split() - if len(parts) < 3: - continue - name_tokens: list[str] = [] - value_token: str | None = None - for tok in parts[1:]: - if _looks_like_number(tok): - value_token = parts[-1] - break - name_tokens.append(tok) - if not name_tokens or value_token is None: - continue - name = "_".join(t.lower() for t in name_tokens) - try: - float(value_token) - except (ValueError, TypeError): - continue - current[name] = value_token - if name not in fields: - fields.append(name) - block_has_data = True - if current: - rows.append(current) - except OSError: - return [], [] - return fields, rows - - def read_residual_rows( runs_dir: Path, cases: Sequence[str], @@ -370,7 +265,12 @@ def render_residuals_svg( restart_iterations: Sequence[float] | None = None, adapter=None, ) -> str: - """Generate an SVG string of residuals vs iteration for selected cases/columns.""" + """Generate an SVG string of residuals vs iteration for selected cases/columns. + + Without `columns`, plots the adapter's default_residual_columns, else the + first two residuals. + """ + adapter = adapter or _default_adapter() header, rows = read_residual_rows( runs_dir, cases, @@ -378,11 +278,11 @@ def render_residuals_svg( include_history=include_history, adapter=adapter, ) - available_cols = [c for c in header if c != "case_id"] - cols = list(columns) if columns else ["velocity"] + available_cols = [c for c in header if c not in {"case_id", "iteration"}] + cols = list(columns) if columns else [c for c in adapter.default_residual_columns if c in available_cols] missing = [c for c in cols if c not in available_cols] if missing: - warn(f"columns missing from residuals.csv: {', '.join(missing)}") + warn(f"columns missing from residuals: {', '.join(missing)}") cols = [c for c in cols if c in available_cols] if not cols: cols = available_cols[:2] if available_cols else [] @@ -402,10 +302,18 @@ def render_residuals_svg( y_min = float("inf") y_max = float("-inf") + # Rows without an iteration are numbered in order, per case. + row_numbers: dict[str, int] = {} + unnumbered = False for row in rows: case_id = row.get("case_id", "") + row_numbers[case_id] = row_numbers.get(case_id, 0) + 1 + iteration = row.get("iteration", "") + if iteration in ("", None): + unnumbered = True + iteration = row_numbers[case_id] try: - x = float(row.get("iteration", "") or 0.0) + x = float(iteration) except ValueError: continue if x < x_from_value: @@ -428,6 +336,8 @@ def render_residuals_svg( if y_max < y: y_max = y + if unnumbered: + warn("some residual rows have no iteration column; they are plotted in row order") if not series: return empty_svg(width, height, "No numeric data to plot") diff --git a/csauto/runner.py b/csauto/runner.py index e6e60f2..210f6b6 100644 --- a/csauto/runner.py +++ b/csauto/runner.py @@ -25,6 +25,7 @@ RuntimeSelection, resolve_runtime, ) +from .logs import is_recent, parse_start_time from .registry import ( STATUS_DONE, STATUS_FAILED, @@ -32,6 +33,7 @@ STATUS_PREPARED, STATUS_RUNNING, append_history, + campaign_case_dirs, load_registry, mutate_registry, registry_transaction, @@ -53,9 +55,11 @@ SLURM_TRUE_VALUES = {"1", "true", "yes", "on"} SLURM_FALSE_VALUES = {"0", "false", "no", "off"} LAUNCH_LOCKFILE = ".csauto.launch.lock" +# Written in the case folder by the launch wrapper when the run command ends. +EXIT_CODE_FILE = ".csauto.exitcode" LAUNCH_THREAD_LOCK = threading.RLock() -RESU_SIZE_CACHE_LOCK = threading.RLock() -RESU_SIZE_CACHE: dict[str, dict[str, float | None]] = {} +RESULTS_SIZE_CACHE_LOCK = threading.RLock() +RESULTS_SIZE_CACHE: dict[str, dict[str, float | None]] = {} try: import fcntl @@ -159,6 +163,8 @@ def _submit_slurm_job( command: Sequence[object], stdout_path: Path, stderr_path: Path, + nprocs: int, + nt: int, env: Mapping[str, str] | None = None, ) -> str: launch_parts = _strip_nohup_prefix(command) @@ -168,6 +174,7 @@ def _submit_slurm_job( env_prefix = " ".join(f"{key}={shlex.quote(value)}" for key, value in sorted((env or {}).items())) if env_prefix: wrapped = f"{env_prefix} {wrapped}" + wrapped = f"{wrapped}; rc=$?; echo $rc > {EXIT_CODE_FILE}; exit $rc" submit_cmd = [ "sbatch", "--parsable", @@ -179,6 +186,10 @@ def _submit_slurm_job( str(stderr_path), "--job-name", f"csauto_{case_dir.name}", + "--ntasks", + str(nprocs), + "--cpus-per-task", + str(nt), "--wrap", wrapped, ] @@ -206,7 +217,7 @@ def _submit_slurm_script_job( stdout_path: Path, stderr_path: Path, ) -> str: - script_path = case_dir / ".csauto.slurm.singularity.sh" + script_path = case_dir / ".csauto.slurm.sh" script_path.write_text(script_content, encoding="utf-8") script_path.chmod(0o755) submit_cmd = [ @@ -312,9 +323,11 @@ def _start_case( return False cidfile = case_dir / ".csauto.cid" if selection.runtime == RUNTIME_DOCKER else None - if cidfile and cidfile.exists(): - with contextlib.suppress(OSError): - cidfile.unlink() + for stale in (cidfile, case_dir / EXIT_CODE_FILE): + if stale and stale.exists(): + with contextlib.suppress(OSError): + stale.unlink() + adapter.prepare_launch(case_dir, nprocs, nt) scheduler = "slurm" if use_slurm_scheduler else None base_update: dict[str, Any] = dict( @@ -430,6 +443,8 @@ def _launch_slurm( cmd, stdout_path, stderr_path, + nprocs, + nt, env=adapter.mpi_env(submit_mpi_exec_options), ) except (OSError, ValueError, RuntimeError) as exc: @@ -458,6 +473,29 @@ def _launch_slurm( ) +def _with_exit_code(command: Sequence[object]) -> list[str]: + """Run `command` through sh, which writes its exit status to EXIT_CODE_FILE when it ends.""" + script = f'"$@"; rc=$?; echo $rc > {EXIT_CODE_FILE}; exit $rc' + return ["nohup", "sh", "-c", script, "csauto-run", *_strip_nohup_prefix(command)] + + +def exit_status_outcome(case_dir: Path, start_time: str | None) -> str | None: + """DONE or FAILED from the exit status of the current run's command, or None when unknown.""" + path = case_dir / EXIT_CODE_FILE + if not path.is_file() or not is_recent(path, parse_start_time(start_time)): + return None + try: + code = int(path.read_text(encoding="utf-8").strip()) + except (OSError, ValueError): + return None + return STATUS_DONE if code == 0 else STATUS_FAILED + + +def final_outcome(adapter: SolverAdapter, case_dir: Path, start_time: str | None) -> str: + """Status of a run that has ended: the solver's own verdict, else its exit status, else FAILED.""" + return adapter.detect_outcome(case_dir, start_time) or exit_status_outcome(case_dir, start_time) or STATUS_FAILED + + def _launch_local( case_dir: Path, case_id: str, @@ -466,6 +504,7 @@ def _launch_local( registry: dict[str, Any], base_update: dict[str, Any], ) -> None: + cmd = _with_exit_code(cmd) stdout_path = case_dir / "csauto.stdout" stderr_path = case_dir / "csauto.stderr" with ExitStack() as _fh: @@ -513,6 +552,7 @@ def _launch_local( start_time=timestamp_now(), end_time=None, pid=proc.pid, + pid_identity=process_identity(proc.pid), job_id=None, ) if cidfile: @@ -543,6 +583,9 @@ def run_cases( adapter: SolverAdapter | None = None, ) -> None: """Launch solver runs for each case directory.""" + # Commands start inside the case folder, so every path handed to them + # (the docker cidfile, the case itself) must be absolute. + runs_dir = runs_dir.resolve() if nprocs <= 0 or nt <= 0: raise ValueError("nprocs and nt must be > 0") if max_parallel <= 0: @@ -577,12 +620,13 @@ def run_cases( raise ValueError(f"Requested cases not found in {runs_dir}: {', '.join(missing)}") case_dirs = [runs_dir / name for name in allowed_list] else: - case_dirs = sorted(p for p in runs_dir.iterdir() if p.is_dir() and p.name.startswith("case")) + case_dirs = campaign_case_dirs(runs_dir) if not case_dirs: - raise ValueError(f"No case* directory found in {runs_dir}") + raise ValueError(f"No case found in {runs_dir} (run csauto prepare first)") launched = 0 + errors: list[str] = [] for case_dir in case_dirs: case_id = case_dir.name @@ -595,6 +639,7 @@ def run_cases( continue if resume_only_failed and status not in {STATUS_FAILED}: continue + previous_status = status or STATUS_PREPARED update_case(registry, case_id, status=STATUS_PENDING) def _do_start(selected_case: Path = case_dir) -> None: @@ -616,7 +661,18 @@ def _do_start(selected_case: Path = case_dir) -> None: ): launched += 1 - _start_case_with_launch_slot(runs_dir, max_parallel, _do_start, adapter) + try: + _start_case_with_launch_slot(runs_dir, max_parallel, _do_start, adapter) + except (OSError, ValueError, RuntimeError) as exc: + # A refused launch (no checkpoint to restart from, a missing input + # file...) must neither strand the case in PENDING nor stop the batch. + errors.append(f"{case_id}: {exc}") + with registry_transaction(runs_dir) as registry: + if registry.get(case_id, {}).get("status") == STATUS_PENDING: + update_case(registry, case_id, status=previous_status) + append_history(case_dir, "launch_error", details={"error": str(exc)}, source=source) + if errors: + raise RuntimeError(f"{len(errors)} case(s) not launched: " + "; ".join(errors)) if launched == 0: print("No cases launched.", file=sys.stderr) @@ -633,28 +689,24 @@ def _count_running_cases(runs_dir: Path, adapter: SolverAdapter) -> int: to_finalize: dict[str, dict[str, Any]] = {} for case_id, record in snapshot.items(): - pid = record.get("pid") - pid_alive = False - if pid is not None: - try: - pid_alive = is_process_alive(int(pid)) - except (TypeError, ValueError): - pid_alive = False - if pid_alive: - still_running.add(case_id) - continue - job_id = _normalize_job_id(record.get("job_id")) - if job_id: - job_active = _is_slurm_job_active(job_id) - if job_active is not False: + alive = _run_alive(record, _is_slurm_job_active) + case_dir = _resolve_case_dir(runs_dir, record.get("path"), case_id) + if alive is None: + # Liveness unknown (squeue unavailable): only a verdict frees the slot. + outcome = adapter.detect_outcome(case_dir, record.get("start_time")) + outcome = outcome or exit_status_outcome(case_dir, record.get("start_time")) + if not outcome: still_running.add(case_id) continue - case_dir = _resolve_case_dir(runs_dir, record.get("path"), case_id) - outcome = adapter.detect_outcome(case_dir, record.get("start_time")) or STATUS_FAILED + elif alive: + still_running.add(case_id) + continue + else: + outcome = final_outcome(adapter, case_dir, record.get("start_time")) to_finalize[case_id] = { "status": outcome, "pid": None, - "job_id": None if job_id else record.get("job_id"), + "job_id": None, "end_time": record.get("end_time") or timestamp_now(), } @@ -706,20 +758,43 @@ def _is_zombie(pid: int) -> bool: return bool(fields) and fields[0] == "Z" -def is_process_alive(pid: int) -> bool: +def process_identity(pid: int) -> str | None: + """ ":" of a process, from procfs, or None without it. + + Unlike wall-clock times, neither part changes when the clock is stepped + (WSL2 resyncs often), at a DST change or with another timezone, so it tells + a launched process from another one that later got the same PID. + """ + try: + boot_id = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="utf-8").strip() + stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8", errors="ignore") + return f"{boot_id}:{stat.rpartition(')')[2].split()[19]}" + except (OSError, IndexError): + return None + + +def is_process_alive(pid: int, identity: str | None = None) -> bool: """Check if a PID is alive (best effort, POSIX-oriented). A zombie counts as dead. `os.kill(pid, 0)` still succeeds for a process that exited but was never reaped, which is what every run launched by the long-lived web server becomes: `run_cases` spawns it with Popen and never waits. Treating that as alive kept finished cases RUNNING forever. + + With `identity` (from process_identity at launch), a PID now held by + another process counts as dead: the PID was reused, for instance after a + reboot, possibly by another user's process. """ + with contextlib.suppress(ChildProcessError, OSError, AttributeError): + # Reaps our own finished child: without /proc (macOS) nothing else tells it ended. + if os.waitpid(pid, os.WNOHANG)[0] == pid: + return False try: os.kill(pid, 0) except ProcessLookupError: return False except PermissionError: - return True + pass # alive but owned by someone else: check the identity below except OSError: return False if _is_zombie(pid): @@ -728,9 +803,28 @@ def is_process_alive(pid: int) -> bool: with contextlib.suppress(ChildProcessError, OSError): os.waitpid(pid, os.WNOHANG) return False + if identity is not None: + current = process_identity(pid) + if current is not None and current != identity: + return False return True +def _run_alive(record: Mapping[str, Any], job_active: Callable[[str], bool | None]) -> bool | None: + """Whether a RUNNING case's process or Slurm job still exists; None when squeue cannot tell.""" + pid = record.get("pid") + if pid is not None: + try: + pid_number = int(pid) + except (TypeError, ValueError): + return False + return is_process_alive(pid_number, identity=record.get("pid_identity")) + job_id = _normalize_job_id(record.get("job_id")) + if job_id: + return job_active(job_id) + return False # nothing left to watch + + def terminate_pid(pid: int, grace: float = 1.0) -> None: """Best-effort terminate a process group (or pid).""" try: @@ -813,33 +907,25 @@ def _compute_refresh_result( job_id = _normalize_job_id(record.get("job_id")) if status == STATUS_RUNNING: - pid_alive = False - if pid is not None: - try: - pid_alive = is_process_alive(int(pid)) - except (TypeError, ValueError): - pid_alive = False - job_active: bool | None = None - if not pid_alive and job_id: - job_active = job_states.get(job_id) - should_finalize = not pid_alive and ((job_id and job_active is False) or (not job_id)) - if should_finalize: - outcome = adapter.detect_outcome(case_dir, start_time) or STATUS_FAILED - status = outcome - end_time = end_time or timestamp_now() - pid = None - job_id = None - if status == STATUS_RUNNING and has_started: - # Even if the PID is stale, trust the log end markers. - outcome = adapter.detect_outcome(case_dir, start_time) + # A run ends when its process or Slurm job is gone, not when its log + # prints a verdict: the solver may still be writing results. Only when + # liveness cannot be told (no PID, squeue unavailable) does a verdict + # end it early. + alive = _run_alive(record, job_states.get) + outcome: str | None = None + if alive is False: + outcome = final_outcome(adapter, case_dir, start_time) + elif alive is None and has_started: + outcome = adapter.detect_outcome(case_dir, start_time) or exit_status_outcome(case_dir, start_time) if outcome: status = outcome - if outcome == STATUS_DONE and not end_time: - end_time = timestamp_now() + end_time = end_time or timestamp_now() pid = None job_id = None - elif status != STATUS_DONE and has_started: - # Only check for completion if a run was actually started. + elif status not in (STATUS_DONE, STATUS_PENDING) and has_started: + # Only check for completion if a run was actually started. A queued + # (PENDING) case still has the previous run's logs, which say nothing + # about the run it waits for. outcome = adapter.detect_outcome(case_dir, start_time) if outcome: status = outcome @@ -1000,16 +1086,16 @@ def _cached_resu_size_mb(case_dir: Path, adapter: SolverAdapter) -> tuple[float except OSError: resu_mtime = None if resu_mtime is None: - with RESU_SIZE_CACHE_LOCK: - RESU_SIZE_CACHE.pop(cache_key, None) + with RESULTS_SIZE_CACHE_LOCK: + RESULTS_SIZE_CACHE.pop(cache_key, None) return None, None - with RESU_SIZE_CACHE_LOCK: - cached = RESU_SIZE_CACHE.get(cache_key) + with RESULTS_SIZE_CACHE_LOCK: + cached = RESULTS_SIZE_CACHE.get(cache_key) if cached and cached.get("mtime") == resu_mtime: return resu_mtime, cached.get("size") size = _resu_size_mb(case_dir, adapter) - with RESU_SIZE_CACHE_LOCK: - RESU_SIZE_CACHE[cache_key] = {"mtime": resu_mtime, "size": size} + with RESULTS_SIZE_CACHE_LOCK: + RESULTS_SIZE_CACHE[cache_key] = {"mtime": resu_mtime, "size": size} return resu_mtime, size diff --git a/csauto/serve_commands.py b/csauto/serve_commands.py index a054b66..804132b 100644 --- a/csauto/serve_commands.py +++ b/csauto/serve_commands.py @@ -8,7 +8,7 @@ def add_serve_subcommands(subparsers: Any, config: Config) -> None: - serve_parser = subparsers.add_parser("serve", help="Primary FastAPI web monitoring server.") + serve_parser = subparsers.add_parser("serve", help="Primary FastAPI web dashboard server.") serve_parser.add_argument("runs_dir", type=Path, help="Directory containing generated cases") serve_parser.add_argument("--host", default=config.host, help="Listening interface (default 127.0.0.1)") serve_parser.add_argument("--port", type=int, default=config.port, help="Listening port (default 8000)") @@ -78,10 +78,13 @@ def dispatch_serve_command( ) try: - from .telemetry import EVENT_SERVE, send_event + from .solvers import get_solver_adapter + from .telemetry import EVENT_SERVE, is_enabled, send_event - print(f"[telemetry] Sending serve ping (type={EVENT_SERVE}, runtime={config.runtime})") - send_event(EVENT_SERVE, valid_result=True, id_docker=config.runtime) + if is_enabled(): + context = f"{get_solver_adapter(config.solver).name}:{config.runtime}" + print(f"[telemetry] Sending serve ping (type={EVENT_SERVE}, context={context})") + send_event(EVENT_SERVE, valid_result=True, id_docker=context) except Exception: pass diff --git a/csauto/solvers/__init__.py b/csauto/solvers/__init__.py index b38e8cc..25ade16 100644 --- a/csauto/solvers/__init__.py +++ b/csauto/solvers/__init__.py @@ -12,7 +12,7 @@ import functools -from .base import SolverAdapter, SolverAdapterBase +from .base import SolverAdapter DEFAULT_SOLVER = "code_saturne" _SOLVER_NAMES = ("code_saturne", "stub", "code_aster") @@ -48,7 +48,6 @@ def _adapter_for(normalized: str) -> SolverAdapter: __all__ = [ "DEFAULT_SOLVER", "SolverAdapter", - "SolverAdapterBase", "available_solvers", "get_solver_adapter", ] diff --git a/csauto/solvers/base.py b/csauto/solvers/base.py index 8b7d83f..07f9b06 100644 --- a/csauto/solvers/base.py +++ b/csauto/solvers/base.py @@ -1,19 +1,22 @@ -"""Solver adapter boundary. - -`SolverAdapter` is the protocol that generic orchestration code (runner, doe, -cli, web routes) programs against; all solver-specific knowledge (command -argv, log parsing, file conventions, control directives) lives behind it. -`SolverAdapterBase` provides shared composition and safe defaults so a -concrete adapter mostly supplies argv fragments, parsers, and convention -names. +"""The solver adapter: everything csauto knows about one solver. + +Generic code (runner, DOE generation, CLI, web routes, dashboard) only talks to +a solver through a `SolverAdapter`. Adding a solver means writing one subclass +and registering it in `csauto/solvers/__init__.py`; nothing else changes. Only +`run_argv`, `find_setup_file` and the attributes marked "required" have to be +written; every other member has a working default. `docs/adding-a-solver.md` +walks through it with `stub.py` as the example. """ from __future__ import annotations +import re from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple + +from ..execution import RUNTIME_DOCKER, RUNTIME_NATIVE, RUNTIME_SINGULARITY if TYPE_CHECKING: from ..execution import RuntimeSelection @@ -21,21 +24,42 @@ class CompareKind(NamedTuple): - """One comparable file in the compare panel; first declared entry is the default.""" + """One comparable file in the Compare panel; the first declared entry is the default.""" - value: str + value: str # name handed to locate_case_file label: str class PerfColumn(NamedTuple): - """One column of the performance/timing table, as rendered by the UI.""" + """One column of the Timing Snapshot table and of the `csauto perf` CSV export.""" - key: str + key: str # key in the dict returned by parse_performance label: str kind: str = "text" # "time" | "int" | "float" | "text" +class ControlAction(NamedTuple): + """A live control directive offered for running cases, applied by apply_control.""" + + name: str + label: str + value_label: str = "" # empty when the action takes no value + value_kind: str = "int" # "int" | "float" + + +class RestartMode(NamedTuple): + """One way to restart a finished case, offered by the Restart dialog.""" + + name: str + label: str + value_label: str = "" # empty when the mode takes no value + value_kind: str = "int" # "int" | "float" + + ALL_DASHBOARD_PANELS = ("status", "residuals", "probes", "performance", "compare", "tail", "errors") +ALL_RUNTIMES = (RUNTIME_NATIVE, RUNTIME_DOCKER, RUNTIME_SINGULARITY) +ANOMALY_LABELS = ("error", "warn") +VALUE_KINDS = ("int", "float") CAPABILITY_RESIDUALS = "residuals" CAPABILITY_PROBES = "probes" @@ -45,170 +69,89 @@ class PerfColumn(NamedTuple): CAPABILITY_RESTART = "restart" CAPABILITY_GUI = "gui" -# Panels any run feeds, whatever the solver: status comes from the registry, -# tail and errors from the csauto.stdout / csauto.stderr launcher logs. -ALWAYS_ON_PANELS = ("status", "tail", "errors") - - -@runtime_checkable -class SolverAdapter(Protocol): - """What the generic core is allowed to know about a solver.""" - - name: str - native_bin_name: str - container_bin_name: str - container_root: str - default_docker_image: str - results_dirname: str - shared_dir_names: tuple[str, ...] - template_input_names: frozenset[str] - anomaly_file_names: tuple[str, ...] - cleanup_log_names: frozenset[str] - performance_fields: tuple[str, ...] - performance_columns: tuple[PerfColumn, ...] - dashboard_panels: tuple[str, ...] - capabilities: frozenset[str] - compare_kinds: tuple[CompareKind, ...] - default_compare_kind: str - control_actions: frozenset[str] - - def run_argv( - self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None - ) -> list[str]: ... - - def gui_argv(self, setup_path: str | Path) -> list[str]: ... - - def build_run_command( - self, - case_dir: Path, - nprocs: int, - nt: int, - selection: RuntimeSelection, - *, - cidfile: Path | None = None, - run_args: Sequence[str] | None = None, - cleanenv: bool = False, - env_vars: Mapping[str, str] | None = None, - ) -> list[str]: ... - - def build_gui_command(self, case_dir: Path, selection: RuntimeSelection) -> list[str]: ... - - def build_slurm_script( - self, - case_dir: Path, - nprocs: int, - nt: int, - selection: RuntimeSelection, - run_args: Sequence[str] | None = None, - env_vars: Mapping[str, str] | None = None, - ) -> str | None: ... - - def mpi_env(self, mpi_exec_options: str | None) -> dict[str, str]: ... - - def build_restart_args( - self, - case_dir: Path, - restart_mode: str | None, - restart_value: int | float | None, - restart_path: str | None, - ) -> tuple[list[str], dict[str, Any]]: ... - - def preflight(self, runs_dir: Path, runtime: str) -> None: ... - - def detect_outcome(self, case_dir: Path, start_time: str | None = None) -> str | None: ... - - def read_progress(self, case_dir: Path, start_time: str | None = None) -> int | None: ... - - def read_restart_origin(self, case_dir: Path) -> dict[str, int | float]: ... - - def locate_case_file(self, case_dir: Path, name: str) -> Path | None: ... - - def results_root(self, case_dir: Path) -> Path: ... - - def list_run_dirs(self, case_dir: Path, *, newest_first: bool = True) -> list[Path]: ... - - def latest_run_dir(self, case_dir: Path) -> Path | None: ... - - def list_result_files(self, case_dir: Path, limit: int = 2000, latest_subdir_only: bool = False) -> list[str]: ... - - def find_setup_file(self, template_dir: Path) -> Path: ... - - def find_run_config(self, template_dir: Path) -> Path | None: ... - - def find_residuals_files(self, case_dir: Path, include_history: bool = False) -> list[Path]: ... - - def parse_live_residuals(self, case_dir: Path) -> tuple[list[str], list[dict[str, str]]]: ... - - def list_probe_files(self, case_dir: Path, limit: int = 200) -> list[str]: ... - - def list_profile_files(self, case_dir: Path, limit: int = 200) -> list[str]: ... - - def locate_probe_files(self, case_dir: Path, probe_ref: str, include_history: bool = False) -> list[Path]: ... - - def find_performance_log(self, case_dir: Path) -> Path | None: ... - - def parse_performance(self, path: Path) -> dict[str, str | None]: ... - - def doctor_checks( - self, - runs_dir: Path, - case_dirs: Sequence[Path], - *, - runtime: str | None = None, - solver_bin: str | None = None, - singularity_image: str | None = None, - singularity_bin: str | None = None, - ) -> list[DoctorItem]: ... - - def apply_control( - self, - case_dir: Path, - action: str, - *, - value: int | None = None, - start_time: str | None = None, - ) -> dict[str, Any]: ... - - -class SolverAdapterBase(ABC): - """Shared composition and safe defaults for solver adapters.""" - - name: ClassVar[str] - native_bin_name: ClassVar[str] - container_bin_name: ClassVar[str] - container_root: ClassVar[str] - default_docker_image: ClassVar[str] - results_dirname: ClassVar[str] - shared_dir_names: ClassVar[tuple[str, ...]] = () - template_input_names: ClassVar[frozenset[str]] = frozenset() - anomaly_file_names: ClassVar[tuple[str, ...]] = ("csauto.stderr", "csauto.stdout") +# Panels any run can feed, whatever the solver: status comes from the registry, +# tail and errors from the csauto.stdout / csauto.stderr launcher logs. Every +# other panel is feedable when the capability of the same name is. +GENERIC_PANELS = frozenset({"status", "tail", "errors"}) + +# Derived from what the adapter implements; declaring them is an error. +_DERIVED_ATTRIBUTES = ("capabilities", "performance_fields", "default_compare_kind") + + +class SolverAdapter(ABC): + """Base class of every solver adapter. See the module docstring.""" + + # Required: identity, binaries and results folder. + name: ClassVar[str] # value of `solver` in csauto.toml + native_bin_name: ClassVar[str] # executable looked up in PATH by the native runtime + container_bin_name: ClassVar[str] # executable started inside docker/apptainer images + default_docker_image: ClassVar[str] # docker image used when csauto.toml sets none + results_dirname: ClassVar[str] # results folder inside each case, e.g. "RESU" + dashboard_panels: ClassVar[tuple[str, ...]] # dashboard tabs, in ALL_DASHBOARD_PANELS order + + # Launch. + supported_runtimes: ClassVar[frozenset[str]] = frozenset(ALL_RUNTIMES) + # Where the campaign folder is mounted inside containers. Keep it out of + # the image user's home (the solver's dotfiles would land in the campaign) + # and away from host paths such as /mnt (WSL drives, data disks). + container_root: ClassVar[str] = "/csauto" + # Shell commands run inside containers before the solver, for images whose + # environment must be activated first (e.g. "source /opt/activate.sh"). + container_setup: ClassVar[str] = "" + + # Case files. + shared_dir_names: ClassVar[tuple[str, ...]] = () # folders next to TEMPLATE shared by every case + readonly_shared_dir_names: ClassVar[frozenset[str]] = frozenset() # mounted read-only in containers + template_input_names: ClassVar[frozenset[str]] = frozenset() # template files never rendered + + # Logs. Names are resolved by locate_case_file; tail_file_names may use globs. + tail_file_names: ClassVar[tuple[str, ...]] = ("csauto.stdout", "csauto.stderr") # Log Tail, best first + anomaly_file_names: ClassVar[tuple[str, ...]] = ("csauto.stderr", "csauto.stdout") # Recent Errors + # (label, pattern) pairs added to logs.GENERIC_ANOMALY_PATTERNS; label is one of ANOMALY_LABELS. + anomaly_patterns: ClassVar[tuple[tuple[str, re.Pattern[str]], ...]] = () + anomaly_ignore_patterns: ClassVar[tuple[re.Pattern[str], ...]] = () # lines never reported cleanup_log_names: ClassVar[frozenset[str]] = frozenset({"csauto.stdout", "csauto.stderr"}) + + # Dashboard declarations. performance_columns: ClassVar[tuple[PerfColumn, ...]] = () compare_kinds: ClassVar[tuple[CompareKind, ...]] = () - control_actions: ClassVar[frozenset[str]] = frozenset() + control_actions: ClassVar[tuple[ControlAction, ...]] = () + restart_modes: ClassVar[tuple[RestartMode, ...]] = () + default_residual_columns: ClassVar[tuple[str, ...]] = () # plotted first, when present + logo_file: ClassVar[Path | None] = None # SVG shown in the dashboard header + icon_file: ClassVar[Path | None] = None # square SVG used as the dashboard's favicon def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) - for reserved in ("dashboard_panels", "capabilities"): - if reserved in cls.__dict__: + for derived in _DERIVED_ATTRIBUTES: + if derived in cls.__dict__: raise TypeError( - f"{cls.__name__} must not declare {reserved!r}: it is derived " + f"{cls.__name__} must not declare {derived!r}: it is derived " f"from what the adapter implements (see docs/adding-a-solver.md)." ) + for label, _pattern in cls.anomaly_patterns: + if label not in ANOMALY_LABELS: + raise TypeError(f"{cls.__name__}.anomaly_patterns: unknown label {label!r} (use {ANOMALY_LABELS})") + for option in (*cls.control_actions, *cls.restart_modes): + if option.value_kind not in VALUE_KINDS: + raise TypeError(f"{cls.__name__}: {option.name!r} has value_kind {option.value_kind!r}") + unknown = set(cls.supported_runtimes) - set(ALL_RUNTIMES) + if unknown: + raise TypeError(f"{cls.__name__}.supported_runtimes: unknown runtimes {sorted(unknown)}") def _provides(self, method_name: str) -> bool: - """True when the adapter defines its own version instead of the empty base default.""" - return getattr(type(self), method_name) is not getattr(SolverAdapterBase, method_name) + """True when the adapter defines its own version instead of the base default.""" + return getattr(type(self), method_name) is not getattr(SolverAdapter, method_name) @property def capabilities(self) -> frozenset[str]: - """What this adapter can actually do, derived from what it provides. + """What this adapter can do, derived from what it provides. - Two forms, chosen per capability according to what the UI consumes: - a redefined method, or an existing declaration that is no longer empty. + A capability is either a redefined method or a non-empty declaration. + The dashboard uses them to show actions, the API to refuse the rest. """ caps: set[str] = set() - if self._provides("find_residuals_files"): + if self._provides("find_residuals_files") or self._provides("parse_live_residuals"): caps.add(CAPABILITY_RESIDUALS) if self._provides("list_probe_files"): caps.add(CAPABILITY_PROBES) @@ -224,12 +167,6 @@ def capabilities(self) -> frozenset[str]: caps.add(CAPABILITY_CONTROL) return frozenset(caps) - @property - def dashboard_panels(self) -> tuple[str, ...]: - """Feedable panels, in ALL_DASHBOARD_PANELS display order.""" - caps = self.capabilities - return tuple(panel for panel in ALL_DASHBOARD_PANELS if panel in ALWAYS_ON_PANELS or panel in caps) - @property def default_compare_kind(self) -> str: """The first declared compare kind, so CLI/API defaults follow the UI order.""" @@ -240,11 +177,35 @@ def performance_fields(self) -> tuple[str, ...]: """CSV export keys, derived from the column metadata so CLI and UI stay consistent.""" return tuple(column.key for column in self.performance_columns) + def control_action(self, name: str) -> ControlAction | None: + return next((action for action in self.control_actions if action.name == name), None) + + def restart_mode(self, name: str) -> RestartMode | None: + return next((mode for mode in self.restart_modes if mode.name == name), None) + + # Launch. + @abstractmethod - def run_argv(self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: - """Solver argv fragment that starts a run, excluding the binary itself.""" + def run_argv(self, case_dir: Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: + """Arguments that start a run, after the solver executable. + + The command starts inside the case folder in every runtime, so use + paths relative to it. `case_dir` is the case folder on this machine, + for reading its files. `nprocs` is the number of MPI processes and `nt` + the number of threads per process; ignore what your solver does not + use. `run_args` are extra arguments, such as those of build_restart_args. + """ + + def prepare_launch(self, case_dir: Path, nprocs: int, nt: int) -> None: + """Called before each launch, to write derived files or create folders. + + Never modify files that came from the template: `csauto prepare` + compares them to detect changed cases. Write new files instead. + """ + return None - def gui_argv(self, setup_path: str | Path) -> list[str]: + def gui_argv(self, setup_path: str) -> list[str]: + """Arguments that open the solver GUI on `setup_path` (relative to the case folder).""" raise ValueError(f"GUI not supported for solver {self.name!r}") def build_run_command( @@ -259,7 +220,7 @@ def build_run_command( cleanenv: bool = False, env_vars: Mapping[str, str] | None = None, ) -> list[str]: - """Compose generic runtime wrapping (docker/native/singularity) around `run_argv`.""" + """Wrap `run_argv` for the selected runtime (native, docker or apptainer).""" from ..execution import build_runtime_run_command return build_runtime_run_command( @@ -275,7 +236,7 @@ def build_run_command( ) def build_gui_command(self, case_dir: Path, selection: RuntimeSelection) -> list[str]: - """Compose generic runtime wrapping around `gui_argv` and the solver setup file.""" + """Wrap `gui_argv` for the selected runtime.""" from ..execution import build_runtime_gui_command return build_runtime_gui_command(case_dir, selection, adapter=self) @@ -289,11 +250,11 @@ def build_slurm_script( run_args: Sequence[str] | None = None, env_vars: Mapping[str, str] | None = None, ) -> str | None: - """Return a full sbatch script, or None to submit the run command via sbatch --wrap.""" + """A full sbatch script, or None to submit the run command with `sbatch --wrap`.""" return None def mpi_env(self, mpi_exec_options: str | None) -> dict[str, str]: - """Environment variables carrying MPI launcher options to the solver.""" + """Environment variables that carry `mpi_exec_options` to the solver on Slurm.""" return {} def build_restart_args( @@ -303,26 +264,69 @@ def build_restart_args( restart_value: int | float | None, restart_path: str | None, ) -> tuple[list[str], dict[str, Any]]: + """Extra run arguments for a restart, and details recorded in the case history. + + `restart_mode` is one of `restart_modes`, already validated. Raise + ValueError with a readable message when the case cannot be restarted. + """ raise ValueError(f"Restart not supported for solver {self.name!r}") def preflight(self, runs_dir: Path, runtime: str) -> None: + """Checks run once before a batch of launches; raise to refuse the batch.""" from ..execution import check_shared_dir_symlinks - check_shared_dir_symlinks(runs_dir, runtime, shared_dirs=self.shared_dir_names) + check_shared_dir_symlinks( + runs_dir, runtime, shared_dirs=self.shared_dir_names, container_root=self.container_root + ) + + # Run state. - @abstractmethod def detect_outcome(self, case_dir: Path, start_time: str | None = None) -> str | None: - """Read solver output to decide STATUS_DONE / STATUS_FAILED / None (still unknown).""" + """STATUS_DONE or STATUS_FAILED read from the solver's output, or None. + + Return None while the output gives no verdict. When the run ends with + no verdict, csauto uses the exit status of the launch command (0 means + DONE), so solvers with meaningful exit codes need no override. Use + `start_time` (logs.parse_start_time / logs.is_recent) to ignore files + left by earlier runs. + """ + return None def read_progress(self, case_dir: Path, start_time: str | None = None) -> int | None: - """Best-effort current iteration / time step of a run.""" + """Current iteration or time step, shown as Last Iter in the status table.""" return None def read_restart_origin(self, case_dir: Path) -> dict[str, int | float]: + """Iteration and/or time the latest run restarted from ({"iteration": ..., "time": ...}).""" return {} + def apply_control( + self, + case_dir: Path, + action: str, + *, + value: int | float | None = None, + start_time: str | None = None, + ) -> dict[str, Any]: + """Apply one of `control_actions` to a running case; return details for the history.""" + raise ValueError(f"Control actions not supported for solver {self.name!r}") + + # Files and results layout. + + @abstractmethod + def find_setup_file(self, template_dir: Path) -> Path: + """The solver's main input file in a template or case folder; raise FileNotFoundError if absent.""" + + def find_run_config(self, template_dir: Path) -> Path | None: + """An optional second input file rendered from the DOE row (code_saturne's run.cfg).""" + return None + def locate_case_file(self, case_dir: Path, name: str) -> Path | None: - """Resolve a friendly file name to a path inside the case directory.""" + """Resolve a friendly file name (a log name, a compare kind) to a file of the case. + + The default resolves paths relative to the case folder. Extend it when + your files live in run folders or under names that change per case. + """ from ..pathutil import safe_subpath direct = safe_subpath(case_dir, name) @@ -334,6 +338,12 @@ def results_root(self, case_dir: Path) -> Path: return case_dir / self.results_dirname def list_run_dirs(self, case_dir: Path, *, newest_first: bool = True) -> list[Path]: + """The folders that each hold one run's results. Clean only ever deletes these. + + The default lists the subfolders of results_root, which suits solvers + that write one folder per run (code_saturne's RESU/). Return + [results_root] when all results go to results_root itself. + """ results_root = self.results_root(case_dir) if not results_root.is_dir(): return [] @@ -348,63 +358,75 @@ def latest_run_dir(self, case_dir: Path) -> Path | None: run_dirs = self.list_run_dirs(case_dir) return run_dirs[0] if run_dirs else None + def find_run_files(self, case_dir: Path, name: str, include_history: bool = False) -> list[Path]: + """`name` in each run folder: the most recent copy, or with include_history every copy oldest first.""" + candidates = [run_dir / name for run_dir in self.list_run_dirs(case_dir) if (run_dir / name).is_file()] + if not include_history: + latest = max(candidates, key=lambda p: p.stat().st_mtime, default=None) + return [latest] if latest else [] + candidates.sort(key=lambda p: (p.parent.stat().st_mtime, p.stat().st_mtime)) + return candidates + def list_result_files(self, case_dir: Path, limit: int = 2000, latest_subdir_only: bool = False) -> list[str]: - """Return result files (no deep subfolders), relative to the case directory.""" - results_root = self.results_root(case_dir) - if not results_root.is_dir(): - return [] - files: list[str] = [] + """Result files relative to the case folder: those of the latest run, or of every run.""" + run_dirs = self.list_run_dirs(case_dir) if latest_subdir_only: - subdirs = [p for p in results_root.iterdir() if p.is_dir()] - if not subdirs: - return [] - latest = max(subdirs, key=lambda p: p.stat().st_mtime) - for path in sorted(latest.iterdir()): - if path.is_file(): - files.append(str(path.relative_to(case_dir))) - if len(files) >= limit: - return files - return files - - for path in sorted(results_root.iterdir()): - if path.is_file(): - files.append(str(path.relative_to(case_dir))) - if len(files) >= limit: - return files - for sub in sorted(p for p in results_root.iterdir() if p.is_dir()): - for path in sorted(sub.iterdir()): + run_dirs = run_dirs[:1] + files: list[str] = [] + for run_dir in run_dirs: + try: + entries = sorted(run_dir.iterdir()) + except OSError: + continue + for path in entries: if path.is_file(): files.append(str(path.relative_to(case_dir))) if len(files) >= limit: return files return files - @abstractmethod - def find_setup_file(self, template_dir: Path) -> Path: - """Locate the solver's primary input file, raising if absent.""" - - def find_run_config(self, template_dir: Path) -> Path | None: - return None + # Analytics. Each returns empty data by default, which hides nothing but + # plots nothing; the capability rules above decide what the UI offers. def find_residuals_files(self, case_dir: Path, include_history: bool = False) -> list[Path]: + """CSV files with an `iteration` column and one column per residual.""" return [] def parse_live_residuals(self, case_dir: Path) -> tuple[list[str], list[dict[str, str]]]: + """Residual rows parsed from a log, used when no residual file exists yet. + + Return (columns, rows); every row needs an "iteration" key. + """ return [], [] def list_probe_files(self, case_dir: Path, limit: int = 200) -> list[str]: + """Probe (time series) names offered by the Probes panel.""" return [] def list_profile_files(self, case_dir: Path, limit: int = 200) -> list[str]: + """Profile (spatial) names offered by the Probes panel.""" return [] def locate_probe_files(self, case_dir: Path, probe_ref: str, include_history: bool = False) -> list[Path]: + """Files holding the probe `probe_ref`: the latest one, or every run's oldest first.""" return [] + def read_probe_file(self, path: Path, max_rows: int | None = None) -> tuple[list[str], list[dict[str, str]]]: + """(columns, rows) of one probe file. The default reads a comma-separated CSV. + + Override it to read another format (SERAFIN, MED...) directly. Time + series need a "t" or "time" column (any case) or an "iteration" column. + max_rows caps the rows returned: 0 when only the columns are needed. + """ + from ..probes import read_csv_table + + return read_csv_table(path, max_rows) + def find_performance_log(self, case_dir: Path) -> Path | None: return None def parse_performance(self, path: Path) -> dict[str, str | None]: + """Timing values keyed like `performance_columns`.""" return {} def doctor_checks( @@ -417,7 +439,7 @@ def doctor_checks( singularity_image: str | None = None, singularity_bin: str | None = None, ) -> list[DoctorItem]: - """Solver-specific pre-run checks; the default verifies each case has a setup file.""" + """Checks shown by `csauto doctor`; the default verifies each case has a setup file.""" from ..maintenance import DoctorItem missing: list[str] = [] @@ -431,13 +453,3 @@ def doctor_checks( suffix = " ..." if len(missing) > 5 else "" return [DoctorItem(level="fail", message=f"solver setup file missing for: {sample}{suffix}")] return [DoctorItem(level="ok", message="solver setup file present in every case")] - - def apply_control( - self, - case_dir: Path, - action: str, - *, - value: int | None = None, - start_time: str | None = None, - ) -> dict[str, Any]: - raise ValueError(f"Control actions not supported for solver {self.name!r}") diff --git a/csauto/solvers/code_aster.py b/csauto/solvers/code_aster.py index f06278d..82c5c3e 100644 --- a/csauto/solvers/code_aster.py +++ b/csauto/solvers/code_aster.py @@ -1,178 +1,99 @@ """code_aster solver adapter. -Launches ``run_aster`` on a case's ``.export`` file inside a container -(docker or apptainer/singularity); the native runtime is not implemented -yet. Case outcome is read from the ``DIAGNOSTIC JOB`` line that code_aster -prints at the end of ``RESU/LOGS/run_solver.log``. +Runs ``run_aster`` on the case's ``.export`` file, from inside the case folder. +In containers the image's environment is activated first (``container_setup``); +natively, ``run_aster`` must be on PATH (or set as ``saturne_bin``). Results go +straight into ``RESU/``, so each case holds one run. The outcome is the +``DIAGNOSTIC JOB`` line run_aster prints on its standard output, which csauto +saves in ``csauto.stdout``. """ from __future__ import annotations import re -import shlex -from collections.abc import Mapping, Sequence +from collections.abc import Sequence from pathlib import Path from typing import ClassVar -from ..execution import RUNTIME_DOCKER, RUNTIME_NATIVE, RUNTIME_SINGULARITY, RuntimeSelection, shared_dir_symlink_mounts -from ..logs import _is_recent, _parse_start_time, read_tail_lines -from ..registry import STATUS_DONE, STATUS_FAILED -from .base import CompareKind, SolverAdapterBase +from ..logs import scan_outcome +from .base import CompareKind, SolverAdapter -CODE_ASTER_EXPORT_EXTENSION = "export" +EXPORT_GLOB = "*.export" +# The export actually run: the case's export with csauto's n and nt, written at +# each launch so the export that came from the template is never modified. +RUN_EXPORT_NAME = ".csauto.export" +OUTCOME_SUCCESS_PATTERNS = (re.compile(r"DIAGNOSTIC JOB : (OK|_ALARM)\b"),) +# Any other verdict (_..., _..., _..., NOOK_TEST_RESU, "?") is a failure. +OUTCOME_FAILURE_PATTERNS = (re.compile(r"DIAGNOSTIC JOB : \S"),) +LOGOS = Path(__file__).with_name("logos") -class CodeAsterAdapter(SolverAdapterBase): +class CodeAsterAdapter(SolverAdapter): name: ClassVar[str] = "code_aster" native_bin_name: ClassVar[str] = "run_aster" - container_bin_name: ClassVar[str] = "" - container_root: ClassVar[str] = "/home/user" + container_bin_name: ClassVar[str] = "run_aster" default_docker_image: ClassVar[str] = "simvia/code_aster:17.4.0" + # Images without /opt/activate.sh are expected to have run_aster on PATH already. + container_setup: ClassVar[str] = "[ ! -f /opt/activate.sh ] || source /opt/activate.sh" results_dirname: ClassVar[str] = "RESU" - logs_dirname: ClassVar[str] = "LOGS" - shared_dir_names: ClassVar[tuple[str, ...]] = ( - "MESH", - "RESU", + dashboard_panels: ClassVar[tuple[str, ...]] = ("status", "compare", "tail", "errors") + # Meshes sit next to TEMPLATE and are referenced from a case as ../MESH/. + shared_dir_names: ClassVar[tuple[str, ...]] = ("MESH",) + readonly_shared_dir_names: ClassVar[frozenset[str]] = frozenset({"MESH"}) + tail_file_names: ClassVar[tuple[str, ...]] = ("csauto.stdout", "*.mess", "csauto.stderr") + # code_aster frames its messages in boxes: "! ...", "! <...>". + anomaly_patterns: ClassVar[tuple[tuple[str, re.Pattern[str]], ...]] = ( + ("warn", re.compile(r"!\s*\s*<")), + ("error", re.compile(r"!\s*<[EFS]>\s*<|")), ) - # doe_row.csv is written into every generated case by the generic DOE code, - # so it is comparable whatever the solver. The .export file would be a better - # candidate but its name varies per case (find_setup_file globs *.export) - # while compare_kinds expects fixed names. - compare_kinds: ClassVar[tuple[CompareKind, ...]] = (CompareKind("doe_row.csv", "doe_row.csv"),) - - def build_run_command( - self, - case_dir: Path, - nprocs: int, - nt: int, - selection: RuntimeSelection, - *, - cidfile: Path | None = None, - run_args: Sequence[str] | None = None, - cleanenv: bool = False, - env_vars: Mapping[str, str] | None = None, - tmp_name: str = "TMP", - ) -> list[str]: - """Build the container command to launch a case.""" - runs_root = case_dir.parent.resolve() - container_root = self.container_root - container_case = f"{container_root}/{case_dir.name}" - export_path = self.find_setup_file(case_dir) - exportfile = export_path.name - host_tmpdir = f"{runs_root}/{case_dir.name}/{tmp_name}" - solverlogpath = f"{self.results_dirname}/{self.logs_dirname}/run_solver.log" - - self._ensure_mess_entry(export_path, solverlogpath) - - links = [f"{runs_root}:{container_root}"] - for name in self.shared_dir_names: - for target, readonly in shared_dir_symlink_mounts(runs_root, (name,)): - linked = f"{target}:{container_case}/{name}" - links.append(f"{linked}:ro" if readonly else linked) - - rmdir = [host_tmpdir] - solver_shell = ( - f"source /opt/activate.sh && run_aster {shlex.quote(exportfile)} --wrkdir {container_case}/{tmp_name}/" - ) - solver_cmd = f"bash -c {shlex.quote(solver_shell)}" - - if selection.runtime == RUNTIME_DOCKER: - add_cid = f"--cidfile {shlex.quote(str(cidfile))} " if cidfile else "" - - bind_links = " ".join(f"-v {shlex.quote(link)}" for link in links) - cleanup = "rm -rf " + " ".join(shlex.quote(dire) for dire in rmdir) - - cmd = [ - "nohup", - "bash", - "-c", - f"docker run " - f"{bind_links} " - f"-w {container_case} " - f"--label csauto.case_id={case_dir.name} " - f"{add_cid}" - f"{selection.docker_image} " - f"{solver_cmd}; " - f"{cleanup}", - ] - elif selection.runtime == RUNTIME_SINGULARITY: - if not selection.singularity_bin or not selection.singularity_image: - raise ValueError("Incomplete singularity configuration.") - host_apptainer = f"{runs_root}/{case_dir.name}/.apptainer_tmp" - rmdir.append(host_apptainer) - - bind_links = " ".join(f"--bind {shlex.quote(link)}" for link in links) - cleanup = "rm -rf " + " ".join(shlex.quote(dire) for dire in rmdir) - - cmd = [ - "nohup", - "bash", - "-c", - f"mkdir -p {shlex.quote(host_apptainer)} && " - f"export APPTAINER_TMPDIR={shlex.quote(host_apptainer)} && " - f"{shlex.quote(selection.singularity_bin)} exec " - f"{bind_links} " - f"--pwd {container_case} " - f"{shlex.quote(selection.singularity_image)} " - f"{solver_cmd}; " - f"{cleanup}", - ] - elif selection.runtime == RUNTIME_NATIVE: - raise NotImplementedError(f"{RUNTIME_NATIVE} must be implemented for code_aster.") - else: - if selection.runtime in ["cave", "salome_meca"]: - raise NotImplementedError( - f"{selection.runtime} could be in the roadmap of code_aster. Please contact support." - ) - else: - raise NotImplementedError(f"{selection.runtime} not in the development roadmap of code_aster.") - - return cmd - - def run_argv(self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: - """Unused: `build_run_command` composes the full launch command itself.""" - return [] - - @staticmethod - def _ensure_mess_entry(export_path: Path, solverlogpath: str) -> None: - """Append the F mess entry that detect_outcome reads, unless the export already declares one.""" - export_text = export_path.read_text(encoding="utf-8", errors="ignore") - if any(line.split()[:2] == ["F", "mess"] for line in export_text.splitlines()): - return - with export_path.open("a", encoding="utf-8") as f: - if export_text and not export_text.endswith("\n"): - f.write("\n") - f.write(f"F mess {solverlogpath} R 6\n") + # run_aster prints this notice before every MPI run; "Abort" would read as an error. + anomaly_ignore_patterns: ClassVar[tuple[re.Pattern[str], ...]] = ( + re.compile(r"If MPI_Abort is called during execution"), + ) + compare_kinds: ClassVar[tuple[CompareKind, ...]] = ( + CompareKind("export", "Export file"), + CompareKind("doe_row.csv", "doe_row.csv"), + ) + logo_file: ClassVar[Path | None] = LOGOS / "code_aster.svg" + icon_file: ClassVar[Path | None] = LOGOS / "code_aster_icon.svg" + + def run_argv(self, case_dir: Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: + return [RUN_EXPORT_NAME, *(str(arg) for arg in run_args or () if str(arg) != "")] + + def prepare_launch(self, case_dir: Path, nprocs: int, nt: int) -> None: + """Write the export to run, with n MPI processes and nt threads, and create RESU.""" + export = self.find_setup_file(case_dir) + lines = [ + line + for line in export.read_text(encoding="utf-8", errors="surrogateescape").splitlines() + if line.split()[:2] not in (["P", "mpi_nbcpu"], ["P", "ncpus"]) + ] + lines += [f"P mpi_nbcpu {nprocs}", f"P ncpus {nt}"] + (case_dir / RUN_EXPORT_NAME).write_text("\n".join(lines) + "\n", encoding="utf-8", errors="surrogateescape") + self.results_root(case_dir).mkdir(exist_ok=True) def find_setup_file(self, template_dir: Path) -> Path: - files = list(Path(template_dir).glob(f"*.{CODE_ASTER_EXPORT_EXTENSION}")) - if len(files) == 1 and files[0].is_file(): - return files[0] - raise FileNotFoundError(f".export file not found in template: {template_dir}") + exports = sorted(p for p in Path(template_dir).glob(EXPORT_GLOB) if p.is_file() and not p.name.startswith(".")) + if len(exports) == 1: + return exports[0] + if not exports: + raise FileNotFoundError(f".export file not found in {template_dir}") + names = ", ".join(p.name for p in exports) + raise ValueError(f"Several .export files in {template_dir} ({names}): keep only one.") def detect_outcome(self, case_dir: Path, start_time: str | None = None) -> str | None: - success_patterns = [ - re.compile(r"DIAGNOSTIC JOB : OK", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _ALARM", re.IGNORECASE), - ] - failure_patterns = [ - re.compile(r"DIAGNOSTIC JOB : _ABNORMAL_ABORT", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _SYNTAX_ERROR", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _MEMORY_ERROR", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _NO_CONVERGENCE", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _CPU_LIMIT", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : _ERROR", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : NO_TEST_RESU", re.IGNORECASE), - re.compile(r"DIAGNOSTIC JOB : NOOK_TEST_RESU", re.IGNORECASE), - ] - log_path = case_dir / self.results_dirname / self.logs_dirname / "run_solver.log" - if not log_path.is_file(): - return None - if not _is_recent(log_path, _parse_start_time(start_time)): - return None - joined = "\n".join(read_tail_lines(log_path, lines=40)) - if any(p.search(joined) for p in success_patterns): - return STATUS_DONE - if any(p.search(joined) for p in failure_patterns): - return STATUS_FAILED - return None + return scan_outcome( + [case_dir / "csauto.stdout"], start_time, OUTCOME_SUCCESS_PATTERNS, OUTCOME_FAILURE_PATTERNS, lines=200 + ) + + def list_run_dirs(self, case_dir: Path, *, newest_first: bool = True) -> list[Path]: + results_root = self.results_root(case_dir) + return [results_root] if results_root.is_dir() else [] + + def locate_case_file(self, case_dir: Path, name: str) -> Path | None: + if name == "export": + try: + return self.find_setup_file(case_dir) + except (FileNotFoundError, ValueError): + return None + return super().locate_case_file(case_dir, name) diff --git a/csauto/solvers/code_saturne.py b/csauto/solvers/code_saturne.py index db70989..7e6621e 100644 --- a/csauto/solvers/code_saturne.py +++ b/csauto/solvers/code_saturne.py @@ -16,25 +16,12 @@ singularity_paths, singularity_shell_exec_prefix, ) -from ..logs import ( - ANOMALY_FILES_DEFAULT, - _is_recent, - _parse_start_time, - detect_run_outcome, - extract_last_iteration, - extract_restart_origin, - extract_run_status_iteration, - find_latest_performance_log, - locate_case_file, - locate_log_file, - locate_run_status_file, - parse_performance_log, -) -from ..probes import list_probe_files, list_profile_files, locate_probe_files +from ..logs import is_recent, parse_start_time, read_tail_lines, scan_outcome +from ..pathutil import is_within_root +from ..probes import list_run_csv_files, locate_run_csv_files from ..registry import STATUS_FAILED -from ..residuals import find_residuals_files, parse_residuals_from_log -from ..template import find_run_cfg, find_setup_file -from .base import CompareKind, PerfColumn, SolverAdapterBase +from ..svg_utils import as_float +from .base import CompareKind, ControlAction, PerfColumn, RestartMode, SolverAdapter if TYPE_CHECKING: from ..execution import RuntimeSelection @@ -55,6 +42,282 @@ # ...and re-echoes it into the main log every time a control_file directive changes it: # " max_time_step 600 (current: 9)\n" (src/base/cs_control.cpp) NT_MAX_CONTROL_RE = re.compile(r"max_time_step\s+(\d+)\s*\(current:") +# Results layout inside each RESU// directory. +PROBES_DIRNAME = "monitoring" +PROFILES_DIRNAME = "profiles" +RESIDUALS_FILENAME = "residuals.csv" +PERFORMANCE_FILENAME = "performance.log" +RUN_STATUS_RUNNING = "run_status.running" +# The solver's main logs, best first: run_solver.log, or listing on older versions. +SOLVER_LOG_NAMES = ("run_solver.log", "listing") +# Friendly names locate_case_file also looks up in the RESU run directories. +RUN_SUMMARY_FILENAME = "summary" +RUN_FILE_NAMES = frozenset( + {*SOLVER_LOG_NAMES, RESIDUALS_FILENAME, RUN_STATUS_RUNNING, PERFORMANCE_FILENAME, RUN_SUMMARY_FILENAME} +) +LOGOS = Path(__file__).with_name("logos") +# Root-level console logs some setups write, searched for progress. +ROOT_LISTING_NAMES = ("listing", "listing.txt", "listing.log", "listing.out") + +OUTCOME_SUCCESS_PATTERNS = tuple( + re.compile(text, re.IGNORECASE) + for text in ( + r"END OF CALCULATION", + r"FINAL STAGE OF THE CALCULATION", + r"CALCULATION COMPLETED", + r"Calculation ended normally", + ) +) +OUTCOME_FAILURE_PATTERNS = (re.compile(r"FATAL ERROR", re.IGNORECASE), re.compile(r"ERROR DETECTED", re.IGNORECASE)) +# code_saturne reports a clean run with "No error detected", which the generic +# error pattern would otherwise flag. +ANOMALY_IGNORE_PATTERNS = (re.compile(r"\bno errors? detected\b", re.IGNORECASE),) +# CFD vocabulary on top of the generic anomaly patterns. +CFD_WARNING_PATTERN = re.compile(r"(divergence|unstable|not converged|cfl|clipping)", re.IGNORECASE) +PROGRESS_PATTERNS = ( + re.compile(r"[Ii]teration\s+(\d+)"), + re.compile(r"[Tt]ime\s+step\s+(\d+)"), + re.compile(r"Iter\s*=\s*(\d+)"), +) +RUN_STATUS_STEP_RE = re.compile(r"time step:\s*(\d+)", re.IGNORECASE) +RESTART_ITER_PATTERNS = ( + re.compile(r"\bnt_prev\b\s*[:=]\s*(\d+)", re.IGNORECASE), + re.compile(r"NUMBER OF THE PREVIOUS TIME STEP\s+nt_prev\s*=\s*(\d+)", re.IGNORECASE), +) +RESTART_TIME_PATTERNS = ( + re.compile(r"\bt_prev\b\s*[:=]\s*([-+0-9.eE]+)", re.IGNORECASE), + re.compile(r"physical time\s+([-+0-9.eE]+)", re.IGNORECASE), +) + + +def _find_input_file(template_dir: Path, name: str) -> Path | None: + """Locate `name` at the case root, in DATA/, or as the single match anywhere below.""" + for candidate in (template_dir / name, template_dir / "DATA" / name): + if candidate.is_file(): + return candidate + matches = list(template_dir.rglob(name)) + if len(matches) > 1: + found = ", ".join(str(p.relative_to(template_dir)) for p in matches[:5]) + suffix = " ..." if len(matches) > 5 else "" + raise ValueError(f"Multiple {name} found in {template_dir}: {found}{suffix}") + return matches[0] if matches else None + + +def parse_residuals_from_log(log_path: Path) -> tuple[list[str], list[dict[str, str]]]: + """Parse the residual blocks code_saturne prints into run_solver.log, for runs without residuals.csv.""" + if not log_path or not log_path.is_file(): + return [], [] + block_idx = 0 + current: dict[str, str] | None = None + block_has_data = False + rows: list[dict[str, str]] = [] + fields: list[str] = ["iteration"] + header_re = re.compile(r"Variable\s+Rhs norm", re.IGNORECASE) + sep_re = re.compile(r"^-{3,}") + try: + with log_path.open("r", encoding="utf-8", errors="ignore") as handle: + for line in handle: + if header_re.search(line): + if current: + rows.append(current) + current = None + block_has_data = False + block_idx += 1 + current = {"iteration": str(block_idx)} + continue + if current is not None: + if sep_re.match(line) and not block_has_data: + continue + if not line.strip() or sep_re.match(line): + if current: + rows.append(current) + current = None + block_has_data = False + continue + parts = line.split() + if len(parts) < 3: + continue + name_tokens: list[str] = [] + value_token: str | None = None + for tok in parts[1:]: + if as_float(tok) is not None: + value_token = parts[-1] + break + name_tokens.append(tok) + if not name_tokens or value_token is None: + continue + name = "_".join(t.lower() for t in name_tokens) + try: + float(value_token) + except (ValueError, TypeError): + continue + current[name] = value_token + if name not in fields: + fields.append(name) + block_has_data = True + if current: + rows.append(current) + except OSError: + return [], [] + return fields, rows + + +PERFORMANCE_FIELDS = ( + "elapsed_time", + "mpi_ranks", + "threads", + "io_time", + "linear_solver_time", + "gradients_time", + "balances_time", +) + + +def parse_performance_log(path: Path) -> dict[str, str | None]: + """Extract timing and parallel metrics from a performance.log file.""" + try: + content = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return dict.fromkeys(PERFORMANCE_FIELDS, None) + + value_pattern = r"([0-9.+\-eE]+)" + patterns = { + "elapsed_time": [ + re.compile(rf"(? int | None: + """The last iteration number printed in the tail of a log, if any.""" + for line in reversed(read_tail_lines(log_path, lines=400)): + for pattern in PROGRESS_PATTERNS: + match = pattern.search(line) + if match: + return int(match.group(1)) + return None + + +def _extract_run_status_iteration(status_path: Path) -> int | None: + """The time step code_saturne records in run_status.running.""" + try: + content = status_path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return None + match = RUN_STATUS_STEP_RE.search(content) + return int(match.group(1)) if match else None + + +def _last_match(lines: Sequence[str], patterns: Sequence[re.Pattern[str]], cast: Any) -> Any: + """The value captured by the last line matching any pattern, converted with `cast`.""" + for raw in reversed(lines): + for pattern in patterns: + match = pattern.search(raw) + if not match: + continue + try: + return cast(match.group(1)) + except (ValueError, TypeError): + continue + return None + + +def _has_checkpoint(run_dir: Path) -> bool: + """True when the run folder holds code_saturne checkpoint files (*.csc).""" + checkpoint_dir = run_dir / "checkpoint" + try: + return checkpoint_dir.is_dir() and any( + child.is_file() and (child.name.endswith(".csc") or ".csc." in child.name) + for child in checkpoint_dir.iterdir() + ) + except OSError: + return False def _require_positive_restart_value( @@ -74,16 +337,27 @@ def _require_positive_restart_value( return v -class CodeSaturneAdapter(SolverAdapterBase): +class CodeSaturneAdapter(SolverAdapter): name: ClassVar[str] = "code_saturne" native_bin_name: ClassVar[str] = "code_saturne" container_bin_name: ClassVar[str] = "code_saturne" - container_root: ClassVar[str] = "/home/code_saturne" default_docker_image: ClassVar[str] = "simvia/code_saturne" results_dirname: ClassVar[str] = "RESU" shared_dir_names: ClassVar[tuple[str, ...]] = ("MESH", "POST") + readonly_shared_dir_names: ClassVar[frozenset[str]] = frozenset({"MESH"}) template_input_names: ClassVar[frozenset[str]] = frozenset({"setup.xml", "run.cfg"}) - anomaly_file_names: ClassVar[tuple[str, ...]] = ANOMALY_FILES_DEFAULT + anomaly_file_names: ClassVar[tuple[str, ...]] = ("csauto.stderr", "run_solver.log", "listing", "csauto.stdout") + anomaly_patterns: ClassVar[tuple[tuple[str, re.Pattern[str]], ...]] = (("warn", CFD_WARNING_PATTERN),) + anomaly_ignore_patterns: ClassVar[tuple[re.Pattern[str], ...]] = ANOMALY_IGNORE_PATTERNS + tail_file_names: ClassVar[tuple[str, ...]] = ( + "run_solver.log", + "listing", + "run_status.running", + "csauto.stdout", + "csauto.stderr", + "performance.log", + RUN_SUMMARY_FILENAME, + ) cleanup_log_names: ClassVar[frozenset[str]] = frozenset( { "run_solver.log", @@ -103,22 +377,44 @@ class CodeSaturneAdapter(SolverAdapterBase): PerfColumn("mpi_ranks", "MPI Ranks", "int"), PerfColumn("threads", "Threads", "int"), ) + dashboard_panels: ClassVar[tuple[str, ...]] = ( + "status", + "residuals", + "probes", + "performance", + "compare", + "tail", + "errors", + ) compare_kinds: ClassVar[tuple[CompareKind, ...]] = ( CompareKind("setup.xml", "setup.xml"), CompareKind("doe_row.csv", "doe_row.csv"), CompareKind("run_solver.log", "run_solver.log"), CompareKind("performance.log", "performance.log"), ) - control_actions: ClassVar[frozenset[str]] = frozenset({"stop", "extend", "checkpoint", "flush"}) + # Directives written to the run's control_file, read by code_saturne at each time step. + control_actions: ClassVar[tuple[ControlAction, ...]] = ( + ControlAction("stop", "Stop gracefully"), + ControlAction("extend", "Extend", "Additional time steps"), + ControlAction("checkpoint", "Write a checkpoint"), + ControlAction("flush", "Flush logs and plots"), + ) + restart_modes: ClassVar[tuple[RestartMode, ...]] = ( + RestartMode("iterations", "Additional iterations", "Iterations"), + RestartMode("physical_time", "Additional physical time", "Time (s)", "float"), + ) + default_residual_columns: ClassVar[tuple[str, ...]] = ("velocity", "pressure") + logo_file: ClassVar[Path | None] = LOGOS / "code_saturne.svg" + icon_file: ClassVar[Path | None] = LOGOS / "code_saturne_icon.svg" - def run_argv(self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: - argv = ["run", "--case", str(case_path), "-n", str(nprocs), "--nt", str(nt)] + def run_argv(self, case_dir: Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: + argv = ["run", "--case", ".", "-n", str(nprocs), "--nt", str(nt)] if run_args: argv.extend(str(arg) for arg in run_args if str(arg) != "") return argv - def gui_argv(self, setup_path: str | Path) -> list[str]: - return ["gui", str(setup_path)] + def gui_argv(self, setup_path: str) -> list[str]: + return ["gui", setup_path] def build_slurm_script( self, @@ -145,7 +441,9 @@ def build_slurm_script( container_setup = f"{container_case}/{setup_rel.as_posix()}" extra_binds = [ f"{target}:{target}:ro" if readonly else f"{target}:{target}" - for target, readonly in shared_dir_symlink_mounts(runs_root, self.shared_dir_names) + for target, readonly in shared_dir_symlink_mounts( + runs_root, self.shared_dir_names, self.readonly_shared_dir_names + ) ] env_flags: list[str] = [] for key, value in sorted((env_vars or {}).items()): @@ -267,28 +565,12 @@ def build_restart_args( f"restart_path must be a run id (e.g. 20260308-0923) or a " f"{self.results_dirname}//checkpoint path" ) + if not _has_checkpoint(self.results_root(case_dir) / restart_run_id): + raise ValueError(f"Run {restart_run_id} of {case_dir.name} has no checkpoint to restart from") else: - resu_root = self.results_root(case_dir) - if resu_root.is_dir(): - try: - resu_dirs = [p for p in resu_root.iterdir() if p.is_dir()] - resu_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) - except OSError: - resu_dirs = [] - for run_dir in resu_dirs: - checkpoint_dir = run_dir / "checkpoint" - if not checkpoint_dir.is_dir(): - continue - try: - has_checkpoint = any( - child.is_file() and (child.name.endswith(".csc") or ".csc." in child.name) - for child in checkpoint_dir.iterdir() - ) - except OSError: - has_checkpoint = False - if has_checkpoint: - restart_run_id = run_dir.name - break + restart_run_id = next( + (run_dir.name for run_dir in self.list_run_dirs(case_dir) if _has_checkpoint(run_dir)), None + ) if not restart_run_id: raise ValueError(f"No checkpoint found for {case_dir.name}") details["restart_run_id"] = restart_run_id @@ -346,39 +628,95 @@ def _extract_restart_checkpoint_state(self, case_dir: Path, run_id: str) -> tupl return None, None def detect_outcome(self, case_dir: Path, start_time: str | None = None) -> str | None: - outcome = detect_run_outcome(case_dir, start_time) + log_dirs = [case_dir, *self.list_run_dirs(case_dir)] + outcome = scan_outcome( + [log_dir / name for log_dir in log_dirs for name in SOLVER_LOG_NAMES], + start_time, + OUTCOME_SUCCESS_PATTERNS, + OUTCOME_FAILURE_PATTERNS, + self.anomaly_ignore_patterns, + ) if outcome: return outcome # The logs gave no verdict. A run that fails before the solver starts (a # missing mesh, say) writes no run_solver.log at all, only a status # marker beside it. Restricted to the current run, so a marker left by a # previous run never overrides the log of this one. - start_ts = _parse_start_time(start_time) + start_ts = parse_start_time(start_time) for run_dir in self.list_run_dirs(case_dir): for name in RUN_STATUS_FAILURE_NAMES: marker = run_dir / name - if marker.is_file() and _is_recent(marker, start_ts): + if marker.is_file() and is_recent(marker, start_ts): return STATUS_FAILED return None def read_progress(self, case_dir: Path, start_time: str | None = None) -> int | None: - run_status_path = locate_run_status_file(case_dir, start_time) - if run_status_path: - return extract_run_status_iteration(run_status_path) - log_path = locate_log_file(case_dir, start_time) - return extract_last_iteration(log_path) if log_path else None + start_ts = parse_start_time(start_time) + markers = [run_dir / RUN_STATUS_RUNNING for run_dir in self.list_run_dirs(case_dir)] + markers = [m for m in markers if m.is_file() and is_recent(m, start_ts)] + if markers: + return _extract_run_status_iteration(max(markers, key=lambda p: p.stat().st_mtime)) + return self.current_iteration(case_dir, start_time) def read_restart_origin(self, case_dir: Path) -> dict[str, int | float]: - return extract_restart_origin(case_dir) + """Iteration and physical time the latest run restarted from, read from its logs.""" + latest = self.latest_run_dir(case_dir) + if latest is None: + return {} + iter_value: int | None = None + time_value: float | None = None + for name in ("setup.log", *SOLVER_LOG_NAMES): + lines = read_tail_lines(latest / name, lines=12000) + if iter_value is None: + iter_value = _last_match(lines, RESTART_ITER_PATTERNS, int) + if time_value is None: + time_value = _last_match(lines, RESTART_TIME_PATTERNS, float) + if iter_value is not None and time_value is not None: + break + result: dict[str, int | float] = {} + if iter_value is not None and iter_value >= 0: + result["iteration"] = iter_value + if time_value is not None and time_value >= 0: + result["time"] = time_value + return result def locate_case_file(self, case_dir: Path, name: str) -> Path | None: - return locate_case_file(case_dir, name) + """Resolve case-relative paths, run files in the newest RESU run that has them, and setup.xml.""" + direct = super().locate_case_file(case_dir, name) + if direct: + return direct + if name in RUN_FILE_NAMES: + root = case_dir.resolve() + for run_dir in self.list_run_dirs(case_dir): + candidate = run_dir / name + if candidate.is_file() and is_within_root(candidate, root): + return candidate + if name == "setup.xml": + try: + return self.find_setup_file(case_dir) + except (FileNotFoundError, ValueError): + return None + return None + + def _locate_log_file(self, case_dir: Path, start_time: str | None = None) -> Path | None: + """The current run's most recently written solver log, else its newest console log.""" + start_ts = parse_start_time(start_time) + candidates = [case_dir / name for name in (*ROOT_LISTING_NAMES, "csauto.stdout")] + candidates += [path for name in SOLVER_LOG_NAMES if (path := self.locate_case_file(case_dir, name))] + current = [p for p in dict.fromkeys(candidates) if p.is_file() and is_recent(p, start_ts)] + if not current: + return None + solver_logs = [p for p in current if p.name in {*SOLVER_LOG_NAMES, *ROOT_LISTING_NAMES}] + return max(solver_logs or current, key=lambda p: p.stat().st_mtime) def find_setup_file(self, template_dir: Path) -> Path: - return find_setup_file(template_dir) + setup_path = _find_input_file(template_dir, "setup.xml") + if setup_path is None: + raise FileNotFoundError(f"setup.xml not found in template: {template_dir}") + return setup_path def find_run_config(self, template_dir: Path) -> Path | None: - return find_run_cfg(template_dir) + return _find_input_file(template_dir, "run.cfg") def doctor_checks( self, @@ -395,7 +733,7 @@ def doctor_checks( missing: list[str] = [] for case_dir in case_dirs: try: - find_setup_file(case_dir) + self.find_setup_file(case_dir) except (FileNotFoundError, ValueError): missing.append(case_dir.name) if missing: @@ -405,25 +743,32 @@ def doctor_checks( return [DoctorItem(level="ok", message="setup.xml present in every case")] def find_residuals_files(self, case_dir: Path, include_history: bool = False) -> list[Path]: - return find_residuals_files(case_dir, include_history=include_history) + return self.find_run_files(case_dir, RESIDUALS_FILENAME, include_history=include_history) def parse_live_residuals(self, case_dir: Path) -> tuple[list[str], list[dict[str, str]]]: - log_path = locate_case_file(case_dir, "run_solver.log") + log_path = self.locate_case_file(case_dir, "run_solver.log") if not log_path: return [], [] return parse_residuals_from_log(log_path) def list_probe_files(self, case_dir: Path, limit: int = 200) -> list[str]: - return list_probe_files(case_dir, limit=limit) + return list_run_csv_files(self.latest_run_dir(case_dir), PROBES_DIRNAME, limit=limit) def list_profile_files(self, case_dir: Path, limit: int = 200) -> list[str]: - return list_profile_files(case_dir, limit=limit) + return list_run_csv_files(self.latest_run_dir(case_dir), PROFILES_DIRNAME, limit=limit, with_prefix=True) def locate_probe_files(self, case_dir: Path, probe_ref: str, include_history: bool = False) -> list[Path]: - return locate_probe_files(case_dir, probe_ref, include_history=include_history) + return locate_run_csv_files( + case_dir, + self.list_run_dirs(case_dir), + (PROBES_DIRNAME, PROFILES_DIRNAME), + probe_ref, + include_history=include_history, + ) def find_performance_log(self, case_dir: Path) -> Path | None: - return find_latest_performance_log(case_dir) + found = self.find_run_files(case_dir, PERFORMANCE_FILENAME) + return found[0] if found else None def parse_performance(self, path: Path) -> dict[str, str | None]: return parse_performance_log(path) @@ -433,7 +778,7 @@ def apply_control( case_dir: Path, action: str, *, - value: int | None = None, + value: int | float | None = None, start_time: str | None = None, ) -> dict[str, Any]: """Drop a code_saturne control_file directive for a running case.""" @@ -459,7 +804,8 @@ def apply_control( self.write_control_directive(case_dir, f"max_time_step {target}") details.update(increment=increment, previous_max_time_step=base, target_time_step=target) else: - raise ValueError(f"Invalid control action: {action!r} (expected one of {sorted(self.control_actions)})") + names = ", ".join(a.name for a in self.control_actions) + raise ValueError(f"Invalid control action: {action!r} (expected one of: {names})") return details def write_control_directive(self, case_dir: Path, directive: str) -> Path: @@ -479,10 +825,8 @@ def write_control_directive(self, case_dir: Path, directive: str) -> Path: def current_iteration(self, case_dir: Path, start_time: str | None = None) -> int | None: """Best-effort read of the case's current time step from its live log.""" - log_path = locate_log_file(case_dir, start_time) - if log_path is None: - return None - return extract_last_iteration(log_path) + log_path = self._locate_log_file(case_dir, start_time) + return _extract_last_iteration(log_path) if log_path else None def configured_max_time_step(self, case_dir: Path) -> int | None: """Best-effort read of the run's actual configured iteration limit (nt_max). diff --git a/frontend/dist/code-aster.svg b/csauto/solvers/logos/code_aster.svg similarity index 100% rename from frontend/dist/code-aster.svg rename to csauto/solvers/logos/code_aster.svg diff --git a/frontend/dist/favicon-code_aster.svg b/csauto/solvers/logos/code_aster_icon.svg similarity index 100% rename from frontend/dist/favicon-code_aster.svg rename to csauto/solvers/logos/code_aster_icon.svg diff --git a/frontend/dist/_app/immutable/assets/code-saturne.BHojVttu.svg b/csauto/solvers/logos/code_saturne.svg similarity index 100% rename from frontend/dist/_app/immutable/assets/code-saturne.BHojVttu.svg rename to csauto/solvers/logos/code_saturne.svg diff --git a/frontend/dist/favicon-code_saturne.svg b/csauto/solvers/logos/code_saturne_icon.svg similarity index 100% rename from frontend/dist/favicon-code_saturne.svg rename to csauto/solvers/logos/code_saturne_icon.svg diff --git a/csauto/solvers/stub.py b/csauto/solvers/stub.py index be5c581..a9dd071 100644 --- a/csauto/solvers/stub.py +++ b/csauto/solvers/stub.py @@ -1,9 +1,10 @@ -"""Fake solver adapter used by integration tests. +"""A fake solver, and the smallest complete example of an adapter. -Runs as a short python script (native runtime with ``saturne_bin`` pointing at -a Python interpreter) that writes ``OUT/run_0001/stub.log`` with one ``step N`` -line per requested step and a final completion marker. It exercises the whole -prepare -> run -> status pipeline without any code_saturne convention on disk. +The "solver" is a short Python script started with the native runtime +(``saturne_bin`` pointing at a Python interpreter). It reads ``steps = N`` from +the case's ``stub.toml`` and writes ``OUT/run_0001/stub.log``: one ``step N`` +line per step, then a completion marker. Integration tests use it to drive +prepare, run, status and control without any real solver installed. """ from __future__ import annotations @@ -12,19 +13,21 @@ from pathlib import Path from typing import Any, ClassVar +from ..pathutil import safe_subpath from ..registry import STATUS_DONE, STATUS_FAILED -from .base import CompareKind, SolverAdapterBase +from .base import CompareKind, ControlAction, SolverAdapter STUB_SETUP_FILENAME = "stub.toml" STUB_LOG_FILENAME = "stub.log" STUB_DONE_MARKER = "STUB CALCULATION COMPLETE" STUB_FAILED_MARKER = "STUB CALCULATION FAILED" +# Started inside the case folder, like every solver command. _STUB_RUN_SCRIPT = """\ -import pathlib, sys -case_dir = pathlib.Path(sys.argv[1]) -steps = int(sys.argv[2]) -run_dir = case_dir / "OUT" / "run_0001" +import pathlib, re +match = re.search(r"steps\\s*=\\s*(\\d+)", pathlib.Path("stub.toml").read_text(encoding="utf-8")) +steps = int(match.group(1)) if match else 3 +run_dir = pathlib.Path("OUT") / "run_0001" run_dir.mkdir(parents=True, exist_ok=True) with (run_dir / "stub.log").open("w", encoding="utf-8") as handle: for step in range(1, steps + 1): @@ -33,24 +36,26 @@ """ -class StubAdapter(SolverAdapterBase): +class StubAdapter(SolverAdapter): + # Required declarations. name: ClassVar[str] = "stub" native_bin_name: ClassVar[str] = "python3" - container_bin_name: ClassVar[str] = "stub" - container_root: ClassVar[str] = "/stub" + container_bin_name: ClassVar[str] = "" default_docker_image: ClassVar[str] = "" results_dirname: ClassVar[str] = "OUT" + dashboard_panels: ClassVar[tuple[str, ...]] = ("status", "compare", "tail", "errors") + + # Optional declarations used by this solver. + supported_runtimes: ClassVar[frozenset[str]] = frozenset({"native"}) template_input_names: ClassVar[frozenset[str]] = frozenset({STUB_SETUP_FILENAME}) + tail_file_names: ClassVar[tuple[str, ...]] = (STUB_LOG_FILENAME, "csauto.stdout", "csauto.stderr") anomaly_file_names: ClassVar[tuple[str, ...]] = ("csauto.stderr", "csauto.stdout", STUB_LOG_FILENAME) cleanup_log_names: ClassVar[frozenset[str]] = frozenset({STUB_LOG_FILENAME, "csauto.stdout", "csauto.stderr"}) compare_kinds: ClassVar[tuple[CompareKind, ...]] = (CompareKind(STUB_SETUP_FILENAME, STUB_SETUP_FILENAME),) - control_actions: ClassVar[frozenset[str]] = frozenset({"stop"}) + control_actions: ClassVar[tuple[ControlAction, ...]] = (ControlAction("stop", "Stop"),) - def run_argv(self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: - argv = ["-c", _STUB_RUN_SCRIPT, str(case_path), str(nt)] - if run_args: - argv.extend(str(arg) for arg in run_args if str(arg) != "") - return argv + def run_argv(self, case_dir: Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: + return ["-c", _STUB_RUN_SCRIPT, *(str(arg) for arg in run_args or () if str(arg) != "")] def find_setup_file(self, template_dir: Path) -> Path: candidate = template_dir / STUB_SETUP_FILENAME @@ -63,8 +68,8 @@ def locate_case_file(self, case_dir: Path, name: str) -> Path | None: if direct: return direct for run_dir in self.list_run_dirs(case_dir): - candidate = run_dir / name - if candidate.is_file(): + candidate = safe_subpath(run_dir, name) # never leave the run folder + if candidate and candidate.is_file(): return candidate return None @@ -88,11 +93,11 @@ def apply_control( case_dir: Path, action: str, *, - value: int | None = None, + value: int | float | None = None, start_time: str | None = None, ) -> dict[str, Any]: if action != "stop": - raise ValueError(f"Invalid control action: {action!r} (expected one of {sorted(self.control_actions)})") + raise ValueError(f"Invalid control action: {action!r} (expected one of: stop)") run_dir = self.latest_run_dir(case_dir) if run_dir is None: raise FileNotFoundError(f"No OUT run directory found for {case_dir.name}") diff --git a/csauto/telemetry.py b/csauto/telemetry.py index 05d9f32..48c73b5 100644 --- a/csauto/telemetry.py +++ b/csauto/telemetry.py @@ -113,7 +113,6 @@ def _send_sync(payload: dict[str, Any]) -> None: TELEMETRY_ENDPOINT, data=data, headers={"Content-Type": "application/json"}, - method="POST", ) with urllib.request.urlopen(req, timeout=TELEMETRY_TIMEOUT_S): pass diff --git a/csauto/template.py b/csauto/template.py index cdea13b..2ab60ef 100644 --- a/csauto/template.py +++ b/csauto/template.py @@ -2,10 +2,12 @@ import re from collections.abc import Mapping -from pathlib import Path -# Match csauto placeholders while ignoring shell expansions like ${HOME}. -PLACEHOLDER_PATTERN = re.compile(r"(?", re.IGNORECASE) COND_END_PATTERN = re.compile(r"", re.IGNORECASE) COND_EXPR_PATTERN = re.compile(r"^([A-Za-z0-9_.-]+)\s*(==|=|!=)\s*(.+)$") @@ -116,44 +118,4 @@ def replace_placeholder(match: re.Match[str]) -> str: joined = ", ".join(sorted(set(remaining))) raise ValueError(f"Unresolved placeholders after rendering for {case_id}: {joined}") - return rendered - - -def find_setup_file(template_dir: Path) -> Path: - """Locate setup.xml in the template directory.""" - candidates = [ - template_dir / "setup.xml", - template_dir / "DATA" / "setup.xml", - ] - for candidate in candidates: - if candidate.is_file(): - return candidate - - matches = list(template_dir.rglob("setup.xml")) - if not matches: - raise FileNotFoundError(f"setup.xml not found in template: {template_dir}") - if len(matches) > 1: - found = ", ".join(str(p.relative_to(template_dir)) for p in matches[:5]) - suffix = " ..." if len(matches) > 5 else "" - raise ValueError(f"Multiple setup.xml found in {template_dir}: {found}{suffix}") - return matches[0] - - -def find_run_cfg(template_dir: Path) -> Path | None: - """Locate run.cfg in the template directory if exactly one candidate exists.""" - candidates = [ - template_dir / "run.cfg", - template_dir / "DATA" / "run.cfg", - ] - for candidate in candidates: - if candidate.is_file(): - return candidate - - matches = list(template_dir.rglob("run.cfg")) - if not matches: - return None - if len(matches) == 1: - return matches[0] - found = ", ".join(str(p.relative_to(template_dir)) for p in matches[:5]) - suffix = " ..." if len(matches) > 5 else "" - raise ValueError(f"Multiple run.cfg found in {template_dir}: {found}{suffix}") + return ESCAPED_PLACEHOLDER_PATTERN.sub(r"\1", rendered) diff --git a/csauto/web_support.py b/csauto/web_support.py index 1ec7daf..1bad643 100644 --- a/csauto/web_support.py +++ b/csauto/web_support.py @@ -8,10 +8,11 @@ from typing import Any from .docker import find_container_id_for_case, read_container_id, terminate_container -from .logs import read_tail_lines +from .execution import RUNTIME_DOCKER +from .logs import find_case_file, read_tail_lines from .pathutil import is_within_root -from .registry import STATUS_FAILED, append_history, load_registry, registry_transaction, timestamp_now, update_case -from .runner import is_process_alive, terminate_pid +from .registry import append_history, load_registry, registry_transaction, timestamp_now, update_case +from .runner import final_outcome, is_process_alive, terminate_pid def _default_adapter() -> Any: @@ -185,8 +186,8 @@ def discover_job_id(case_dir: Path, job_id_patterns: Sequence[re.Pattern[str]], seen: set[str] = set() candidates: list[Path] = [] - for name in ("csauto.stdout", "csauto.stderr", "run_solver.log", "listing"): - file_path = adapter.locate_case_file(case_dir, name) + for name in adapter.anomaly_file_names: + file_path = find_case_file(case_dir, name, adapter) if not file_path or not file_path.is_file(): continue key = str(file_path.resolve()) @@ -239,7 +240,7 @@ def count_running_cases(runs_dir: Path, status_running: str) -> int: pid_alive = False if pid is not None: try: - pid_alive = is_process_alive(int(pid)) + pid_alive = is_process_alive(int(pid), identity=record.get("pid_identity")) except (TypeError, ValueError): pid_alive = False if pid_alive: @@ -300,8 +301,8 @@ def kill_case( container_id = read_container_id(cidfile, wait=1.0) if container_id: record["container_id"] = container_id - if not container_id: - container_id = find_container_id_for_case(case_id) + if not container_id and record.get("runtime") == RUNTIME_DOCKER: + container_id = find_container_id_for_case(case_id, runs_dir) if container_id: record["container_id"] = container_id @@ -314,6 +315,7 @@ def kill_case( start_time = record.get("start_time") previous_end_time = record.get("end_time") pid_raw = record.get("pid") + pid_identity = record.get("pid_identity") if pid_raw is None and not container_id and not job_id: raise ValueError(f"No PID, container_id or job_id for {case_id}") @@ -338,7 +340,7 @@ def kill_case( pid_int = int(pid_raw) except (TypeError, ValueError) as exc: raise ValueError(f"Invalid PID for {case_id}") from exc - if is_process_alive(pid_int): + if is_process_alive(pid_int, identity=pid_identity): terminate_pid(pid_int) pid_alive = True details["pid_killed"] = True @@ -357,7 +359,7 @@ def kill_case( if job_error and pid_raw is None and not container_id: raise job_error - outcome = adapter.detect_outcome(case_dir, start_time) or STATUS_FAILED + outcome = final_outcome(adapter, case_dir, start_time) with registry_transaction(runs_dir) as registry: update_case( registry, diff --git a/docs/README.md b/docs/README.md index 995d206..ea4d26c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,33 +2,33 @@ ## Getting started -1. [Core concepts](./concepts.md) — understand the DOE/template/cases mental model before anything else -2. [Quickstart](./quickstart.md) — step-by-step first run, local and Slurm +1. [Core concepts](./concepts.md): understand the DOE/template/cases mental model before anything else +2. [Quickstart](./quickstart.md): step-by-step first run, local and Slurm ## Daily use (web UI) The web UI is the recommended interface for all daily operations. -- [Web UI guide](./web-ui.md) — launch, monitor, restart, stop/extend/checkpoint, kill, clean from the browser -- [Task cookbook](./task-cookbook.md) — copy/paste recipes, UI-first with CLI alternatives -- [Run lifecycle](./run-lifecycle.md) — case states, transitions, and state files +- [Web UI guide](./web-ui.md): launch, monitor, restart, live control, kill and clean from the browser +- [Task cookbook](./task-cookbook.md): copy/paste recipes, UI-first with CLI alternatives +- [Run lifecycle](./run-lifecycle.md): case states, transitions, and state files ## Reference -- [Configuration](./config.md) — `csauto.toml` keys, environment variables, priority rules -- [DOE format](./doe-format.md) — CSV format, placeholders, IF blocks -- [Input/output formats](./formats.md) — files read and written by csauto -- [HTTP API](./api.md) — REST endpoints, request conventions, cURL examples -- [CLI reference](./cli.md) — all commands and options (for scripting and setup steps) -- [Limitations](./limitations.md) — known constraints and best practices +- [Configuration](./config.md): `csauto.toml` keys, environment variables, priority rules +- [DOE format](./doe-format.md): CSV format, placeholders, IF blocks +- [Input/output formats](./formats.md): files read and written by csauto +- [HTTP API](./api.md): REST endpoints, request conventions, cURL examples +- [CLI reference](./cli.md): all commands and options (for scripting and setup steps) +- [Limitations](./limitations.md): known constraints and best practices ## Troubleshooting -- [Troubleshooting](./troubleshooting.md) — error messages, diagnostics, common issues +- [Troubleshooting](./troubleshooting.md): error messages, diagnostics, common issues ## Contributing - [Architecture](./architecture.md) -- [Adding a new solver](./adding-a-solver.md) — implement a `SolverAdapter` for another solver +- [Adding a new solver](./adding-a-solver.md): implement a `SolverAdapter` for another solver - [Contributing guide](./contributing.md) - [Test suite](../tests/README.md) diff --git a/docs/adding-a-solver.md b/docs/adding-a-solver.md index 371c5a6..35b4ab6 100644 --- a/docs/adding-a-solver.md +++ b/docs/adding-a-solver.md @@ -1,161 +1,264 @@ # Adding a new solver -csauto talks to solvers exclusively through the `SolverAdapter` boundary -(see [Architecture — Solver adapter boundary](./architecture.md#solver-adapter-boundary)). -Adding a solver means writing one adapter class and registering its name — -the core (runner, DOE generation, CLI, web routes) does not change. +csauto knows a solver only through its **adapter**: one Python class that +says how to start the solver, where its files are, and how to read its +output. Everything else (case generation, launching on a laptop, a container +or Slurm, status tracking, the web dashboard) is shared by every solver. -Two reference implementations exist: +Adding a solver means writing one adapter class, registering its name and +adding a sample case to the test suite. No other part of csauto changes, +including the dashboard. -- `csauto/solvers/stub.py` — the minimal possible adapter (a fake solver used - by integration tests). Start by reading this one. -- `csauto/solvers/code_saturne.py` — a full production adapter with restart, - Slurm scripts, residuals/probes/performance parsing, and live control. +Three adapters ship with csauto: -## 1. Create the adapter class +| Adapter | Read it for | +|---|---| +| `csauto/solvers/stub.py` | The smallest complete adapter: a fake solver used by the tests. Start here. | +| `csauto/solvers/code_aster.py` | A real solver in about 100 lines: environment setup in containers, results written straight into `RESU/`, verdict read from the solver's standard output. | +| `csauto/solvers/code_saturne.py` | The most complete adapter: residuals, probes, timing, restart, live control, GUI, a custom Slurm script. | -Create `csauto/solvers/.py` and subclass `SolverAdapterBase` -(`csauto/solvers/base.py`), which provides working defaults for most of the -protocol. +## 1. Write the adapter -Three methods are abstract and must be implemented: +Create `csauto/solvers/.py` with a subclass of `SolverAdapter`. This +is a complete adapter for a solver started as `mysolver input.cfg`, which +writes each run's results into a new folder under `results/`: -| Method | Purpose | -|---|---| -| `run_argv(case_path, nprocs, nt, run_args)` | Solver argv fragment that starts a run, excluding the binary itself. The generic runtime wrapping composes it into `nohup ...` (native), `docker run ... ...`, or `apptainer exec ... ...`. | -| `detect_outcome(case_dir, start_time)` | Read the solver's output and return `STATUS_DONE`, `STATUS_FAILED`, or `None` (still unknown). This is how csauto decides a run finished. | -| `find_setup_file(template_dir)` | Locate the solver's primary input file in a template or case directory, raising `FileNotFoundError` if absent. Used by `prepare`, `doctor`, and the GUI. | - -These class attributes have no default and must be set: - -| Attribute | Example (code_saturne) | Used for | -|---|---|---| -| `name` | `"code_saturne"` | identity, error messages | -| `native_bin_name` | `"code_saturne"` | PATH lookup for the `native` runtime | -| `container_bin_name` | `"code_saturne"` | argv inside singularity containers | -| `container_root` | `"/home/code_saturne"` | bind-mount target inside containers | -| `default_docker_image` | `"simvia/code_saturne"` | `docker` runtime default | -| `results_dirname` | `"RESU"` | per-case results directory | - -Then declare the conventions that differ from the base defaults: - -- `template_input_names` — files rendered per case and skipped by the generic - render sweep. -- `shared_dir_names` — template-level shared dirs copied/symlinked into the - runs dir. -- `cleanup_log_names` — logs the Clean action may truncate. -- `anomaly_file_names` — launcher/solver logs scanned for anomalies, and the - file list offered by the Recent Errors panel. -- `compare_kinds` — `CompareKind(value, label)` entries that populate the - compare panel's file selector. The first entry is the default; - `default_compare_kind` is derived from it, so do not declare it yourself. -- `performance_columns` — `PerfColumn(key, label, kind)` entries (`kind` is - `"time"`, `"int"`, `"float"`, or `"text"`) that define the Timing Snapshot - table. The CSV export keys (`performance_fields`) are derived from them, so - do not declare those either. -- `control_actions` — the live-control directives your `apply_control` - implements (code_saturne declares `stop`, `extend`, `checkpoint`, `flush`). -## Dashboard panels are derived, not declared - -You never declare which panels your solver gets: `dashboard_panels` and -`capabilities` are computed from what your adapter actually provides, and -declaring either of them raises `TypeError` at import time. - -| Capability | Granted when your adapter | -|---|---| -| `residuals` | overrides `find_residuals_files` | -| `probes` | overrides `list_probe_files` | -| `restart` | overrides `build_restart_args` | -| `gui` | overrides `gui_argv` | -| `compare` | declares a non-empty `compare_kinds` | -| `performance` | declares a non-empty `performance_columns` | -| `control` | declares a non-empty `control_actions` | - -The `status`, `tail` and `errors` panels are always present: they are fed by the -registry and by the `csauto.stdout` / `csauto.stderr` launcher logs, which exist -for every solver. - -Implement what your solver can feed and the panel appears; implement nothing and -the panel, along with its action buttons, is absent from the dashboard and -refused by the API with a 400. Run `csauto doctor RUNS` to see what was derived: +```python +from collections.abc import Sequence +from pathlib import Path +from typing import ClassVar -``` -[OK] solver code_aster: panels status, compare, tail, errors -[OK] solver code_aster: capabilities compare +from .base import SolverAdapter + + +class MySolverAdapter(SolverAdapter): + name: ClassVar[str] = "mysolver" # the value of `solver` in csauto.toml + native_bin_name: ClassVar[str] = "mysolver" # looked up in PATH by the native runtime + container_bin_name: ClassVar[str] = "mysolver" # started inside docker and apptainer images + default_docker_image: ClassVar[str] = "example/mysolver:1.0" + results_dirname: ClassVar[str] = "results" + dashboard_panels: ClassVar[tuple[str, ...]] = ("status", "tail", "errors") + + def run_argv(self, case_dir: Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]: + return ["input.cfg", "--procs", str(nprocs), *(run_args or [])] + + def find_setup_file(self, template_dir: Path) -> Path: + path = template_dir / "input.cfg" + if not path.is_file(): + raise FileNotFoundError(f"input.cfg not found in {template_dir}") + return path ``` -The web UI reads `panels`, `capabilities`, `control_actions`, `compare_kinds` -and `anomaly_file_names` from `/api/app_config`, and the timing columns from -`/api/perf`, so these declarations reshape the dashboard without any frontend -change. - -Everything else is optional. Useful overrides, from most to least common: - -- `read_progress(case_dir, start_time)` — current iteration for the status - table (base returns `None`). -- `locate_case_file(case_dir, name)` — friendly-name file resolution for the - web UI; the base only resolves paths relative to the case directory, so - extend it if your results live in run subdirectories (see `StubAdapter`). -- `gui_argv(setup_path)` — argv fragment to open the solver GUI (base raises - "GUI not supported"). -- `build_slurm_script(...)` — return a full sbatch script when your solver - cannot be launched via `sbatch --wrap` (base returns `None`, meaning wrap). -- `mpi_env(mpi_exec_options)` — env vars carrying MPI options to the solver. -- `build_restart_args(...)` / `apply_control(...)` — checkpoint restart and - live control directives (base raises "not supported", which surfaces as a - clean error in the UI). -- `find_residuals_files` / `parse_live_residuals` / probe listing / - `find_performance_log` / `parse_performance` — analytics (base returns - empty, so charts degrade gracefully). -- `doctor_checks(...)` — the base already verifies every case has a setup - file; override to add solver-specific environment checks. - -Results-layout helpers (`results_root`, `list_run_dirs`, `latest_run_dir`, -`list_result_files`) are derived from `results_dirname` and normally need no -override, as long as your solver writes one subdirectory per run under it. - -## 2. Register the solver +That is all csauto needs: + +- **`run_argv`** returns the arguments after the solver executable. The + command always starts **inside the case folder**, in every runtime, so use + paths relative to it. `case_dir` is the case folder on this machine, for + when you need to read a file to build the arguments. `nprocs` is the number + of MPI processes and `nt` the number of threads per process: use them, + translate them, or ignore them. +- **`find_setup_file`** returns the solver's main input file. `csauto prepare` + renders it from each DOE row, and `csauto doctor` checks every case has one. +- **How a run ends.** When the run command exits, csauto reads its exit + status: 0 means DONE, anything else FAILED. If your solver's exit status + is not reliable, override `detect_outcome` (see + [the recipe](#the-solver-prints-its-verdict-in-a-log)). + +## 2. Register it In `csauto/solvers/__init__.py`, add the name to `_SOLVER_NAMES` and a branch in `_adapter_for()`: ```python -_SOLVER_NAMES = ("code_saturne", "openfoam", "stub") +_SOLVER_NAMES = ("code_saturne", "stub", "code_aster", "mysolver") + + if normalized == "mysolver": + from .mysolver import MySolverAdapter - if normalized == "openfoam": - from .openfoam import OpenFoamAdapter + return MySolverAdapter() +``` + +Select it in the campaign's `csauto.toml` (`solver = "mysolver"`). +`csauto prepare` records it in `RUNS/campaign.json`, so later commands use the +right adapter from any directory. + +## 3. Test it + +Add a `sample_mysolver(case_dir)` function to +`tests/unit/test_adapter_conformance.py`. It writes a small **finished** case, +the way your solver leaves it: the input file, the logs, the results. The +conformance suite then checks your adapter against it and says, in plain +words, what the dashboard would miss: an unknown tab, a log file the Log Tail +offers but cannot open, a capability with nothing behind it, a finished run +that does not read as DONE. + +``` +pytest tests/unit/test_adapter_conformance.py -q +``` + +Then run a real campaign: `csauto prepare`, `csauto run`, `csauto status`, +and `csauto serve` to look at the dashboard. If your solver has a docker +image, add it to `tests/integration/test_docker_solvers.py`, which runs the +shipped examples for real when `CSAUTO_DOCKER_TESTS=1` is set. + +`csauto doctor RUNS` shows what your adapter offers: - return OpenFoamAdapter() ``` +[OK] solver mysolver: panels status, tail, errors +[OK] solver mysolver: capabilities none +[OK] solver mysolver: runtimes docker, native, singularity +``` + +## 4. Fill the dashboard + +The dashboard shows the tabs listed in `dashboard_panels`, in this order: +`status`, `residuals`, `probes`, `performance`, `compare`, `tail`, `errors`. +Status, Log Tail and Recent Errors work for every solver. List another tab +once your adapter feeds it; the conformance suite refuses a tab nothing feeds. -Config validation picks the new name up automatically from -`available_solvers()`. +| Tab | What to add | +|---|---| +| Log Tail | Nothing. It offers `csauto.stdout` and `csauto.stderr` (the solver's console output) plus every `*.log` file of the latest run. Set `tail_file_names` to put your main logs first; entries may be globs, such as `"*.mess"`. | +| Recent Errors | Nothing. It scans `anomaly_file_names` for crashes, tracebacks, `error:` lines and warnings. Add your solver's own markers with `anomaly_patterns` (labels `"error"` or `"warn"`) and silence false positives with `anomaly_ignore_patterns`. The Log Tail colours lines with the same patterns. | +| Compare | `compare_kinds`: `CompareKind(value, label)` entries. The value is a name handed to `locate_case_file`, so it can be an alias for a file whose name changes per case (code_aster maps `"export"` to the case's `.export`). The first entry is the default. | +| Residuals | `find_residuals_files` returning CSV files with an `iteration` column and one column per residual, or `parse_live_residuals` returning rows parsed from a log. `default_residual_columns` names the curves shown first. | +| Probes | `list_probe_files` and `locate_probe_files`. Time series need a `t` or `time` column. Files are read as comma-separated CSV; override `read_probe_file(path, max_rows)` for another format (return at most `max_rows` rows when it is set: 0 means only the column names are needed). `list_profile_files` adds spatial profiles. | +| Timing Snapshot | `performance_columns` (`PerfColumn(key, label, kind)`, kind `"time"`, `"int"`, `"float"` or `"text"`), `find_performance_log`, and `parse_performance` returning values under those keys. `csauto perf` exports the same columns. | + +The status table's **Last Iter** column comes from `read_progress`. + +**Actions.** Run, Kill and Clean work for every solver. These ones appear +only when the adapter provides them: + +| Button | What to add | +|---|---| +| Restart | `build_restart_args` and `restart_modes`: `RestartMode(name, label, value_label, value_kind)` entries, one per way to restart (code_saturne offers additional iterations or additional physical time). The dialog asks for a value when `value_label` is set. | +| Live control | `control_actions` (`ControlAction(name, label, value_label, value_kind)`) and `apply_control`. They appear in the dashboard's control menu and as `csauto control RUNS CASE ACTION [VALUE]`. | +| Open GUI | `gui_argv(setup_path)`: the arguments that open the solver's GUI on the setup file. | + +**Branding.** Point `logo_file` at an SVG for the dashboard header and +`icon_file` at a square SVG for the browser tab; keep them in +`csauto/solvers/logos/`. Without them the header shows the solver's name. -## 3. Select it +## Recipes -```toml -# csauto.toml -solver = "openfoam" +### The solver prints its verdict in a log + +Override `detect_outcome` and let `csauto.logs.scan_outcome` do the reading: +it looks at the end of each file, skips files written before the current run +started, and returns DONE, FAILED or None (no verdict yet). + +```python +DONE = (re.compile(r"Normal end of run"),) +FAILED = (re.compile(r"Fatal error"),) + +def detect_outcome(self, case_dir, start_time=None): + return scan_outcome([case_dir / "csauto.stdout"], start_time, DONE, FAILED) ``` -The CLI and the web server build the adapter from this key; no other -configuration changes. +A run stays RUNNING until its process or Slurm job is gone, even after the +verdict is printed; the verdict then decides the status. When the logs give +no verdict, csauto falls back to the exit status. + +### The image needs its environment activated + +Set `container_setup` to the shell commands to run in the container before +the solver (code_aster: `"[ ! -f /opt/activate.sh ] || source /opt/activate.sh"`, run by `bash`). When the image has no +ENTRYPOINT, csauto starts the solver with `--entrypoint`; an image whose own +ENTRYPOINT starts the solver gets only `run_argv`, so that script sets up the +environment instead of `container_setup`. +For native runs, the solver must already be on PATH, or `saturne_bin` in +`csauto.toml` must point to it. -## 4. Test it +### Results go in one folder per run, or straight into the results folder -- Unit-test the argv fragments and outcome/progress parsing against golden - values (see `tests/unit/test_solvers.py`). -- Copy the pattern of `tests/integration/test_stub_adapter.py`: generate cases - from a template, launch with `runtime="native"` pointing at a harmless - binary, and poll `refresh_status` until the adapter reports `DONE`. -- `tests/unit/test_solver_boundary.py` will fail if your work leaks solver - literals into the generic modules — route everything through the adapter. +`list_run_dirs` returns the folders that each hold one run's results. Clean +only ever deletes these folders, and "Keep latest N" counts them. The default +lists the subfolders of `results_dirname`, which suits solvers that create a +new folder per run (code_saturne's `RESU/`). If your solver writes +everything into `results_dirname` itself, return `[self.results_root(case_dir)]` +(code_aster does): each case then holds one run, and a new run replaces it. -## 5. Optional: dashboard branding +`locate_case_file(case_dir, name)` turns the names used by the Log Tail, +Recent Errors and Compare into files. The default resolves paths relative to +the case folder; extend it when your logs live in run folders (see +`StubAdapter` and `CodeSaturneAdapter`). These names can come from API +requests: build paths with `pathutil.safe_subpath(folder, name)`, which refuses +`..` and absolute paths, as the stub does. csauto also ignores any file you +return from outside the case folder. -The dashboard header shows your solver's `name` as text (only code_saturne -ships a header logo today; adding one means editing -`frontend/src/lib/components/hero/HeroBanner.svelte`). The favicon defaults to -the Simvia mark and switches to `/favicon-.svg` automatically when that -asset exists — drop a `favicon-.svg` into `frontend/static/` and rebuild -the frontend to ship one. +### The solver needs a file written at launch + +Do it in `prepare_launch(case_dir, nprocs, nt)`, called right before each +launch. Never modify a file that came from the template: `csauto prepare` +compares those to the template to detect changed cases. Write a new file +instead. code_aster writes `.csauto.export`, a copy of the case's export +carrying the run's `n` and `nt`, and runs that one. + +### Template files contain braces + +`{name}` in a template is a DOE placeholder. When braces belong to the input +language (Python f-strings or sets in a code_aster `.comm` file), write `\{i}` +to keep a literal `{i}`. To keep csauto away from a whole file, list its name +in `template_input_names`. + +### Slurm + +By default csauto submits the run command with `sbatch --wrap`, requesting +`--ntasks` from `n` and `--cpus-per-task` from `nt`. When your solver needs a +real batch script (code_saturne's apptainer runs stage, compute and finalize +in separate steps), return it from `build_slurm_script`. `mpi_env` turns the +`mpi_exec_options` setting into environment variables for the solver. + +### Runtimes + +`supported_runtimes` lists the runtimes the solver can use (`native`, +`docker`, `singularity`). Auto mode only picks among them, and asking for +another one is refused before anything starts. Containers mount the campaign +folder at `container_root` (`/csauto` by default). Keep it out of the image +user's home directory, or the solver's own settings and caches end up in the +campaign folder. + +## Reference + +Required (no default): + +| Member | Meaning | +|---|---| +| `name` | value of `solver` in `csauto.toml` | +| `native_bin_name` | executable looked up in PATH by the native runtime | +| `container_bin_name` | executable started inside container images | +| `default_docker_image` | image used when `csauto.toml` sets no `docker_image` | +| `results_dirname` | results folder inside each case | +| `dashboard_panels` | dashboard tabs | +| `run_argv(case_dir, nprocs, nt, run_args)` | arguments after the executable | +| `find_setup_file(template_dir)` | the main input file | + +Optional, with their default: + +| Member | Default | +|---|---| +| `supported_runtimes` | native, docker and singularity | +| `container_root` | `"/csauto"` | +| `container_setup` | none | +| `shared_dir_names`, `readonly_shared_dir_names` | none: folders next to `TEMPLATE` shared by every case (code_saturne: `MESH`, `POST`), reached as `../MESH` from a case | +| `template_input_names` | none: files never rendered | +| `tail_file_names` | `csauto.stdout`, `csauto.stderr` | +| `anomaly_file_names` | `csauto.stderr`, `csauto.stdout` | +| `anomaly_patterns`, `anomaly_ignore_patterns` | none | +| `cleanup_log_names` | `csauto.stdout`, `csauto.stderr`: logs Clean may shorten | +| `compare_kinds`, `performance_columns`, `control_actions`, `restart_modes`, `default_residual_columns` | none | +| `logo_file`, `icon_file` | none | +| `prepare_launch` | does nothing | +| `detect_outcome` | no verdict: the exit status decides | +| `read_progress`, `read_restart_origin` | no value | +| `locate_case_file` | paths relative to the case folder | +| `list_run_dirs` | subfolders of `results_dirname`, newest first | +| `find_run_config` | none: a second rendered input file (code_saturne's `run.cfg`) | +| `build_slurm_script`, `mpi_env` | `sbatch --wrap`, no variables | +| `doctor_checks` | every case has a setup file | +| analytics methods | empty results | + +`capabilities` (what the dashboard and the API allow), `performance_fields` +and `default_compare_kind` are derived from the members above; declaring +them is an error. diff --git a/docs/api.md b/docs/api.md index 17d469f..68fe2d9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -17,17 +17,26 @@ Interactive API documentation is available at: - `http://127.0.0.1:8000/docs` (Swagger UI) - `http://127.0.0.1:8000/redoc` (ReDoc) +The server works with the solver the campaign was prepared for (recorded in +`RUNS/campaign.json`). Several responses depend on it: `GET /api/app_config` +lists what the solver offers, and a restart, control or GUI request the solver +cannot perform is refused with `400` (for example `Solver 'code_aster' does not +support restart`). + ## Authentication Authentication is optional. -If `[api].token` is configured in `csauto.toml`, each `/api/*` request must send either: +If `[api].token` is configured in `csauto.toml` (or `serve --token` is given), each `/api/*` request must send either: - `X-CSAUTO-TOKEN: ` - `Authorization: Bearer ` Without a valid token, the server returns `401 Unauthorized`. +`GET /api/solver_logo` and `GET /api/solver_icon` need no token, like the +dashboard's own assets. + ## Request conventions Case list query format: @@ -37,10 +46,11 @@ Case list query format: - repeated query: `?case=case0001&case=case0002` - comma-separated: `?case=case0001,case0002` -Boolean query values: +Boolean query values (any case): -- true values: `1`, `true`, `yes` -- any other value is treated as false +- true: `1`, `true`, `yes`, `on`, `t`, `y` +- false: `0`, `false`, `no`, `off`, `f`, `n` +- any other value, empty included, is refused with `422` Case validation: @@ -49,6 +59,71 @@ Case validation: ## GET endpoints +## `GET /api/app_config` + +Purpose: + +- describe what the campaign's solver offers, so a client shows only what works + +Response (code_saturne): + +```json +{ + "solver": "code_saturne", + "panels": ["status", "residuals", "probes", "performance", "compare", "tail", "errors"], + "capabilities": ["compare", "control", "gui", "performance", "probes", "residuals", "restart"], + "compare_kinds": [ + { "value": "setup.xml", "label": "setup.xml" }, + { "value": "doe_row.csv", "label": "doe_row.csv" }, + { "value": "run_solver.log", "label": "run_solver.log" }, + { "value": "performance.log", "label": "performance.log" } + ], + "error_files": ["csauto.stderr", "run_solver.log", "listing", "csauto.stdout"], + "tail_files": ["run_solver.log", "listing", "run_status.running", "csauto.stdout", "csauto.stderr", "performance.log", "summary"], + "control_actions": [ + { "name": "stop", "label": "Stop gracefully", "value_label": "", "value_kind": "int" }, + { "name": "extend", "label": "Extend", "value_label": "Additional time steps", "value_kind": "int" }, + { "name": "checkpoint", "label": "Write a checkpoint", "value_label": "", "value_kind": "int" }, + { "name": "flush", "label": "Flush logs and plots", "value_label": "", "value_kind": "int" } + ], + "restart_modes": [ + { "name": "iterations", "label": "Additional iterations", "value_label": "Iterations", "value_kind": "int" }, + { "name": "physical_time", "label": "Additional physical time", "value_label": "Time (s)", "value_kind": "float" } + ], + "default_residual_columns": ["velocity", "pressure"], + "logo": true, + "icon": true +} +``` + +Fields: + +- `panels`: dashboard tabs, in display order +- `capabilities`: what the solver can do (`residuals`, `probes`, `performance`, + `compare`, `control`, `restart`, `gui`). Restart, control and GUI requests the + solver does not support are refused with `400`; the residuals, probes and + timing endpoints of a solver without them return empty results +- `compare_kinds`: files offered by Compare; the first one is the default `kind` of `/api/compare_runs` +- `error_files`: files `/api/recent_errors` scans by default +- `tail_files`: Log Tail file names, best first; entries may be globs such as + `*.mess`, and `/api/tail_files` resolves them for one case +- `control_actions`, `restart_modes`: an empty `value_label` means no value is + taken; otherwise `value_kind` (`int` or `float`) says which number is expected +- `default_residual_columns`: residual curves plotted first +- `logo`, `icon`: whether `/api/solver_logo` and `/api/solver_icon` have an image + +For code_aster, `panels` is `status`, `compare`, `tail`, `errors`, the only +capability is `compare`, and `control_actions` and `restart_modes` are empty. + +## `GET /api/solver_logo` and `GET /api/solver_icon` + +Purpose: + +- the solver's logo (dashboard header) and square icon (browser tab) + +No authentication. Response content-type: `image/svg+xml`, or `404` when the +solver has no such image. + ## `GET /api/status` Purpose: @@ -74,7 +149,7 @@ Response: "last_iter": 178, "duration_s": 522, "duration": "8m42s", - "last_mod": "2026-03-09T10:02:11", + "last_mod": "2026-03-09T10:02:11+00:00", "resu_size_mb": 128.4, "doe": { "density_value": "1.1" @@ -85,11 +160,13 @@ Response: } ``` +`last_mod` is a UTC timestamp. + ## `GET /api/perf` Purpose: -- collect performance records from selected cases +- collect timing records from selected cases Query parameters: @@ -97,23 +174,22 @@ Query parameters: Response: -- `{ "records": [...] }` - -## `GET /api/residuals` - -Purpose: - -- return residual rows as JSON - -Query parameters: - -- `case` (required) -- `limit` (optional int, default `0` = no limit) -- `include_history` (optional boolean): include all RESU runs - -Response: +```json +{ + "columns": [ + { "key": "elapsed_time", "label": "Elapsed (s)", "kind": "time" }, + { "key": "mpi_ranks", "label": "MPI Ranks", "kind": "int" } + ], + "records": [ + { "case_id": "case0001", "elapsed_time": "1.967", "mpi_ranks": null } + ] +} +``` -- `{ "header": [...], "records": [...] }` +`columns` are the solver's timing columns (`kind` is `time`, `int`, `float` or +`text`). Each record holds `case_id` plus one value per column `key`, `null` +when the log does not give it. Cases without a timing log have no record. Both +lists are empty for a solver without timing columns (code_aster). ## `GET /api/restart_origin` @@ -138,9 +214,9 @@ Purpose: Query parameters: - `case` (required) -- `files` (optional comma list): defaults to internal anomaly files -- `max_hits` (optional int, default `200`, clamped `1..500`) -- `context` (optional int, default `6`, clamped `0..50`): lines before and after hit +- `files` (optional comma list): defaults to the solver's `error_files` (see `/api/app_config`) +- `max_hits` (optional int `1..500`, default `200`) +- `context` (optional int `0..50`, default `6`): lines before and after hit - `sev` (optional): `all`, `error`, `warn`, `info`, or comma mix - `q` (optional): plain text filter @@ -177,7 +253,7 @@ Purpose: Query parameters: - `case` (required) -- `columns` (optional comma/space list) +- `columns` (optional comma/space list, default: the solver's `default_residual_columns`, else the first two residuals) - `width` (optional int, default `900`) - `height` (optional int, default `500`) - `x_min` (optional float, default `0`) @@ -196,73 +272,104 @@ Purpose: Query parameters: - `case` (required) -- `file` (optional, default `listing`) +- `file` (optional, default: the first file `/api/tail_files` lists for the case) - `n` (optional int, default `200`) Response content-type: - `text/plain` -## `GET /api/resu_files` +## `GET /api/tail_files` Purpose: -- list files in latest RESU run +- list the log files the Log Tail panel can show for one case, best first Query parameters: - `case` (required) -- `limit` (optional int, default `2000`) Response: -- `{ "files": [...] }` +```json +{ "files": ["run_solver.log", "csauto.stdout", "csauto.stderr", "performance.log", "summary", "RESU/20261007-1437/setup.log"] } +``` -## `GET /api/resu_dirs` +The solver's `tail_files` that exist come first; glob entries become paths +relative to the case. The other `*.log` files of the latest run follow. + +## `GET /api/tail_lines` Purpose: -- list RESU run directories sorted by recency +- return the last lines of a case file, each with the severity the solver's + anomaly patterns give it (what the Log Tail colours) Query parameters: - `case` (required) +- `file` (optional, same default as `/api/tail`) +- `n` (optional int, default `200`) Response: -- `{ "dirs": ["20260308-1413", "20260308-1347", ...] }` +```json +{ + "file": "run_solver.log", + "lines": [ + { "text": " Time step 12", "severity": null }, + { "text": "Warning: clipping of k", "severity": "warn" } + ] +} +``` -## `GET /api/probes` +`severity` is `error`, `warn` or `null`. + +## `GET /api/resu_files` Purpose: -- list available probe/profile files for one case +- list the files of the case's latest run folder, relative to the case Query parameters: - `case` (required) -- `scope` (optional): `monitoring` (default) or `profiles` -- `limit` (optional int, default `200`) +- `limit` (optional int, default `2000`) Response: - `{ "files": [...] }` -## `GET /api/probe_data` +## `GET /api/resu_dirs` Purpose: -- return probe CSV records +- list the case's run folders, newest first: the folders the solver reports as + runs (code_saturne: each `RESU/`; code_aster: `RESU` itself) Query parameters: - `case` (required) -- `probe` (required) + +Response: + +- `{ "dirs": ["20260308-1413", "20260308-1347", ...] }` + +## `GET /api/probes` + +Purpose: + +- list the probe/profile files of one or more cases (names merged, no duplicates) + +Query parameters: + +- `case` (required) +- `scope` (optional): `probes` (default) or `profiles` - `limit` (optional int, default `200`) Response: -- `{ "header": [...], "records": [...] }` +- `{ "files": [...] }` ## `GET /api/probe_position` @@ -290,7 +397,7 @@ Purpose: Query parameters: - `case` (required) -- `probe` or `probes` (required, comma or repeated) +- `probe` (required, comma or repeated) Response: @@ -305,7 +412,7 @@ Purpose: Query parameters: - `case` (required) -- `probe` or `probes` (required) +- `probe` (required, comma or repeated) - `axis` (optional, default `time`) - `columns` (optional comma/space list) - `width` (optional int, default `900`) @@ -327,7 +434,10 @@ Purpose: Query parameters: - `case` (required) -- `kind` (required): example `setup.xml`, `doe_row.csv`, `listing`, ... +- `kind` (required): a path relative to the case (`doe_row.csv`, + `DATA/setup.xml`), or a name the solver resolves: code_saturne `setup.xml` + and run files such as `run_solver.log` (from the latest run that has them), + code_aster `export` Response content-type: @@ -343,80 +453,41 @@ Query parameters: - `case` (required) - `base` (optional, default first selected case) -- `kind` (optional, default `setup.xml`) +- `kind` (optional, default: the solver's first compare kind, `setup.xml` for + code_saturne, `export` for code_aster) - `filter` (optional regex) Response content-type: - `text/plain` diff output -## `GET /api/case_diff` - -Purpose: - -- diff a file between exactly two cases - -Query parameters: - -- `case1` (required) -- `case2` (required) -- `kind` (required) - -Response content-type: - -- `text/plain` - -## `GET /api/history` +## `GET /api/settings/telemetry` Purpose: -- read per-case action history - -Query parameters: - -- `case` (required) -- `limit` (optional int, default `100`, clamped `1..1000`) -- `include_refresh` (optional boolean, default false) -- `actions` (optional comma list to include) -- `q` (optional text filter) +- read whether anonymous usage telemetry is enabled Response: -- `{ "items": [...] }` - -## POST endpoints - -## `POST /api/highlight_xml` - -Purpose: - -- return highlighted HTML for XML content - -Payload: - ```json -{ "content": "..." } +{ "enabled": true } ``` -Response content-type: - -- `text/html` +## POST endpoints -## `POST /api/highlight_diff` +## `POST /api/settings/telemetry` Purpose: -- return highlighted HTML for unified diff content +- enable or disable anonymous usage telemetry Payload: ```json -{ "content": "@@ ..." } +{ "enabled": false } ``` -Response content-type: - -- `text/html` +Response: the new setting, as for `GET /api/settings/telemetry`. ## `POST /api/case_file` @@ -511,7 +582,12 @@ Payload example: Rules: - `cases` is required -- `keep_resu` and `delete_resu` are mutually exclusive +- `keep_resu` and `delete_resu` are mutually exclusive; they name run folders as + `/api/resu_dirs` lists them +- only the run folders the solver reports are deleted (code_saturne: each + `RESU/`; code_aster: `RESU` itself, one run per case) +- `RUNNING` and `PENDING` cases are skipped +- with `prune_resu`, a `DONE` or `FAILED` case left without any run returns to `PREPARED` Response: @@ -540,6 +616,8 @@ Payload: Behavior: - multi-case kill runs in parallel +- docker runs: the case's container is stopped (found from `.csauto.cid`, else + by its labels among this campaign's containers) - returns `500` if any selected case fails to stop Success response: @@ -552,28 +630,32 @@ Success response: Purpose: -- send a live steering directive to one or more running cases, by dropping a - `control_file` into the active `RESU//` directory (code_saturne polls - it once per time step) — an alternative to killing the process +- send one of the solver's live control actions to one or more running cases, + an alternative to killing the process. code_saturne drops a `control_file` + into the active `RESU//` directory, which it polls once per time step. Payload: ```json -{ "cases": ["case0001", "case0002"], "action": "stop", "value": null } +{ "cases": ["case0001", "case0002"], "action": "extend", "value": 500 } ``` -- `action`: one of `"stop"`, `"extend"`, `"checkpoint"`, `"flush"` -- `value`: required positive integer for `"extend"` (additional time steps); ignored otherwise +- `action`: one of the solver's `control_actions` (see `/api/app_config`); + code_saturne: `stop`, `extend`, `checkpoint`, `flush` +- `value`: a number, required (and positive) when the action has a + `value_label`, refused otherwise; a whole number when its `value_kind` is + `int`. Only code_saturne's `extend` takes one (additional time steps). Behavior: +- `400` for a solver without live control, an unknown action, or a bad `value` - multi-case control runs in parallel -- every selected case must currently be `RUNNING`, or the request fails +- every selected case must currently be `RUNNING` - `extend` raises the case's actual configured iteration limit (as code_saturne reports it, not csauto's own `--nt`/OpenMP thread count) by `value`; repeated extends stack correctly -- returns `500` if any selected case fails (not `RUNNING`, no active RESU - directory, invalid `value` for `extend`, etc.) +- returns `500` if any selected case fails (not `RUNNING`, no active run + folder, etc.) - each action is logged to the case's `.csauto.history.jsonl` Success response: @@ -586,7 +668,7 @@ Success response: Purpose: -- launch code_saturne GUI for one case on server side +- launch the solver's GUI (code_saturne) for one case on server side Payload: @@ -596,6 +678,7 @@ Payload: Notes: +- `400` for a solver without a GUI - requires server `DISPLAY` - runtime resolution errors are returned as `500` @@ -640,8 +723,22 @@ Rules: - `cases`, `n`, `nt` are required - `n`, `nt`, `max_parallel` must be integers `> 0` -- `restart_mode` accepted values: `iterations`, `physical_time` -- `restart_value` must match selected mode constraints +- without `max_parallel`, every selected case starts at once (`max_parallel` in + `csauto.toml` only applies to `csauto run`) +- a restart needs the `restart` capability (code_saturne only) +- `restart_mode` must be one of the solver's `restart_modes` (see + `/api/app_config`); code_saturne: `iterations`, `physical_time` +- `restart_value` must be positive, and a whole number for an `int` mode; it + needs a `restart_mode` +- without `restart_mode`, code_saturne restarts with the limits already in the case's setup +- `restart_path` (optional) picks the run to restart from: a run folder name + (`20260308-1413`) or a `RESU//checkpoint` path. By default code_saturne + restarts from the latest run that has a checkpoint. + +A case the solver refuses to launch (a restart without checkpoint, say) keeps +its status, and the other selected cases still launch. The response is then +`500` and names the refused cases: +`Launch error: 1 case(s) not launched: case0003: No checkpoint found for case0003`. Success response: @@ -654,9 +751,10 @@ Success response: Common status codes: - `200`: success -- `400`: missing/invalid parameters +- `400`: missing/invalid parameters, or an action the solver does not support - `401`: unauthorized (token) - `404`: resource not found +- `422`: a parameter of the wrong type or out of range (for example `"n": 1.5`) - `500`: internal/runtime/launch errors Common error messages: @@ -666,7 +764,11 @@ Common error messages: - `Missing case, probe parameters` - `Missing case, kind, content parameters` - `n, nt, and max_parallel must be integers > 0` -- `restart_mode must be iterations or physical_time` +- `Solver 'code_aster' does not support restart` +- `Unknown restart_mode 'iter' (expected: iterations, physical_time)` +- `iterations needs a whole number (Iterations)` +- `Invalid action (expected one of: stop, extend, checkpoint, flush)` +- `extend needs a positive value (Additional time steps)` ## cURL examples @@ -683,6 +785,12 @@ curl -s -H "X-CSAUTO-TOKEN: my-token" \ "http://127.0.0.1:8000/api/status" ``` +See what the campaign's solver offers: + +```bash +curl -s "http://127.0.0.1:8000/api/app_config" +``` + Fetch residual SVG: ```bash diff --git a/docs/architecture.md b/docs/architecture.md index a50bf97..bc2c4aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,14 +15,14 @@ The web UI is the primary interface for daily operations. After `prepare` and op - `csauto/cli.py` - CLI argument parsing and command dispatch - `csauto/config.py` - `csauto.toml` loading and `Config` class -- `csauto/solvers/` - solver adapter boundary: protocol, code_saturne adapter, test stub (see below) +- `csauto/solvers/` - solver adapters: the `SolverAdapter` base class, code_saturne, code_aster, the test stub (see below) - `csauto/doe.py` - DOE parsing and case generation - `csauto/template.py` - placeholder and IF rendering - `csauto/runner.py` - launch, restart, kill, status transitions -- `csauto/control.py` - live steering (stop/extend/checkpoint/flush) orchestration, directives applied by the adapter +- `csauto/control.py` - live control orchestration; the actions are declared and applied by the adapter - `csauto/execution.py` - runtime resolution and generic runtime command wrapping (docker/native/singularity) - `csauto/docker.py` - Docker container commands (run, GUI, terminate) -- `csauto/logs.py` - code_saturne log parsing (outcome, anomalies, tails, restart origin), used via the adapter +- `csauto/logs.py` - generic log engine: tails, the incremental anomaly scanner, outcome scanning, recency checks - `csauto/residuals.py` - residual collection and SVG rendering - `csauto/probes.py` - probes/profiles collection and SVG rendering - `csauto/svg_utils.py` - shared SVG rendering primitives (axes, ticks, legends) @@ -41,80 +41,73 @@ The web UI is the primary interface for daily operations. After `prepare` and op ## Solver adapter boundary -All solver-specific knowledge lives behind the `SolverAdapter` protocol -(`csauto/solvers/base.py`). The generic core (runner, doe, cli, control, web -routes) never references solver conventions directly; a source-scan test -(`tests/unit/test_solver_boundary.py`) enforces this. The adapter is selected -by the `solver` key in `csauto.toml` (default `code_saturne`) and threaded -through entry points: the CLI builds it once in `main`, the web app stores it -on the shared `FastAPIContext` as `ctx.adapter`. Generic functions accept an -optional `adapter` argument and fall back to the code_saturne adapter, so -direct calls keep working. +csauto knows each solver through one class, a subclass of `SolverAdapter` +(`csauto/solvers/base.py`). The rest of csauto holds the machinery that is the +same for every solver, and the adapter supplies what differs: how to start the +solver, where its files are, and how to read its output. Generic mechanism, +solver-declared vocabulary. ``` csauto/solvers/__init__.py # available_solvers(), get_solver_adapter(name) -csauto/solvers/base.py # SolverAdapter protocol + SolverAdapterBase defaults +csauto/solvers/base.py # SolverAdapter: declarations, hooks and their defaults csauto/solvers/code_saturne.py # CodeSaturneAdapter +csauto/solvers/code_aster.py # CodeAsterAdapter csauto/solvers/stub.py # StubAdapter (fake solver used by integration tests) +csauto/solvers/logos/ # solver logos and icons served to the dashboard ``` -An adapter supplies: - -- **Commands**: `run_argv`/`gui_argv` fragments composed by the generic - runtime wrapping in `execution.py`/`docker.py`, plus `build_slurm_script` - for solvers that need a full batch script instead of `sbatch --wrap`, and - `mpi_env` for scheduler-time environment variables. -- **Restart**: `build_restart_args` (checkpoint discovery and solver restart - flags). -- **Outcome and progress**: `detect_outcome` (DONE/FAILED from solver output) - and `read_progress` (current iteration). -- **File conventions**: setup/run-config discovery, `results_dirname`, - shared template dirs (`MESH`/`POST` for code_saturne), the friendly-name - file catalog (`locate_case_file`), results listing, cleanup log names. -- **Analytics**: residuals/probes/performance discovery and parsing (SVG - rendering stays generic in `svg_utils.py`). -- **Dashboard surface**: derived, not declared. `capabilities` is computed from - what the adapter provides (an overridden `find_residuals_files`, - `list_probe_files`, `build_restart_args` or `gui_argv`, or a non-empty - `compare_kinds`, `performance_columns` or `control_actions`), and - `dashboard_panels` follows from it. Both are served to the frontend by - `/api/app_config` and enforced server side by - `FastAPIContext.require_capability`. An adapter that declares either attribute - raises `TypeError` at import time. `performance_columns` also drives the - Timing Snapshot columns and the performance CSV export keys, `compare_kinds` - the compare panel's file list (first entry is the default), and - `anomaly_file_names` doubles as the Recent Errors file list. -- **Control**: `apply_control` for live stop/extend/checkpoint/flush - directives, with `control_actions` declaring what the solver supports. -- **Doctor**: `doctor_checks` for solver-specific environment validation. - -Ownership across the runtime x solver matrix: docker mounts and labels, -singularity exec prefixes, `sbatch --wrap` submission, local `Popen`, process -and container termination, launch slots, and the registry are runtime/generic -concerns and stay outside the adapter. The one deliberate exception is the -3-stage singularity Slurm script (`stage/initialize -> srun cs_solver -> -finalize`), which encodes code_saturne workflow knowledge and is owned -entirely by `CodeSaturneAdapter.build_slurm_script`. - -Accepted residue, by design: the HTTP API keeps its historical names and -shapes (`/api/resu_dirs`, `resu_removed`, the performance record fields), TOML -keys and CLI flags (`saturne_bin`, `--saturne-bin`, ...) are unchanged, and -`config.py` keeps the code_saturne defaults. +The adapter is chosen by the `solver` key of `csauto.toml`. `csauto prepare` +records it in `RUNS/campaign.json`, and every later command on that campaign +uses the recorded solver, whatever directory it runs from. The CLI builds the +adapter once in `main`; the web app keeps it on `FastAPIContext` as +`ctx.adapter`. + +What stays generic, and what the adapter supplies: + +| Concern | Generic code | Adapter | +|---|---|---| +| Case generation | DOE parsing, placeholder and IF rendering, shared dirs (`doe.py`, `template.py`) | the setup file and run config, shared dir names, files never rendered | +| Launch | runtime selection, native/docker/apptainer wrapping with the case folder as working directory, `sbatch --wrap` with `--ntasks`/`--cpus-per-task`, exit status recording (`execution.py`, `docker.py`, `runner.py`) | `run_argv`, `container_bin_name`, `container_setup`, `supported_runtimes`, `prepare_launch`, optional `build_slurm_script` and `mpi_env` | +| Run state | liveness of the process or Slurm job, the registry, launch slots; a run ends when its process ends | `detect_outcome` (else the exit status decides), `read_progress`, `read_restart_origin` | +| Results | Clean, results size, file listings | `results_dirname`, `list_run_dirs` (the only folders Clean deletes), `locate_case_file` | +| Logs | tailing, the incremental anomaly scanner, `scan_outcome` (`logs.py`) | log names (`tail_file_names`, `anomaly_file_names`), anomaly patterns and ignores | +| Analytics | CSV reading, SVG rendering (`residuals.py`, `probes.py`, `svg_utils.py`) | where the residual, probe, profile and timing files are, and how to parse them | +| Dashboard | every panel and button, driven by `/api/app_config` | `dashboard_panels`, compare kinds, timing columns, control actions, restart modes, default residual curves, logo and icon | + +The adapter's `capabilities` (residuals, probes, performance, compare, +control, restart, gui) are derived from what it implements. The dashboard +shows only the actions an adapter can perform, and the API refuses the others +with a 400 (`FastAPIContext.require_capability`). + +Every registered adapter must pass `tests/unit/test_adapter_conformance.py`, +which checks its declarations against a sample finished case. +`tests/unit/test_solver_boundary.py` scans every Python module outside +`csauto/solvers/` for code_saturne file names and log markers; it catches +names, not behaviour, which is what the conformance suite and the shared +tests are for. + +Accepted residue, listed in `ACCEPTED_RESIDUE` in +`tests/unit/test_solver_boundary.py`: the HTTP API keeps its historical names +(`/api/resu_dirs`, `/api/resu_files`, `resu_removed`, `resu_size_mb`, the +`profiles` scope of `/api/probes`), and the TOML key and CLI flag +`saturne_bin` / `--saturne-bin` (the native solver executable, whatever the +solver) keep their names. `StubAdapter` (`solver = "stub"`) is a shipped fake solver: it runs a short -Python script writing `OUT/run_0001/stub.log` and lets integration tests cover -prepare -> run -> status -> control end to end without a real solver installed. +Python script writing `OUT/run_0001/stub.log` and lets the tests cover +prepare, run, status and control without a real solver installed. Adding a solver means writing one adapter class and registering it in -`csauto/solvers/__init__.py`; the core does not change. See -[Adding a new solver](./adding-a-solver.md) for the step-by-step guide. +`csauto/solvers/__init__.py`. See [Adding a new solver](./adding-a-solver.md). ## Persistent data model - `RUNS/registry.json` - global case state +- `RUNS/campaign.json` - the solver the campaign was prepared for - `RUNS/caseXXXX/doe_row.csv` - resolved DOE row per case - `RUNS/caseXXXX/.csauto.history.jsonl` - per-case action history - `RUNS/caseXXXX/.csauto.jobid` - scheduler job id when relevant +- `RUNS/caseXXXX/.csauto.exitcode` - exit status of the last run command ## Runtime model @@ -127,8 +120,12 @@ Supported runtimes: Launch modes: -- local detached process (`nohup`/`Popen`) -- Slurm submission (`sbatch --wrap`, monitored via `squeue`) +- local detached process (`nohup`/`Popen`), started inside the case folder +- Slurm submission (`sbatch --wrap` or an adapter-built script, monitored via `squeue`) + +Containers mount the campaign folder at the adapter's `container_root` +(`/csauto` by default) and run with `--rm`; symlinked shared dirs are mounted at +their own path so `../MESH` resolves inside the container. ## Testing strategy diff --git a/docs/cli.md b/docs/cli.md index f87a61f..30d3d9f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,11 +1,11 @@ # CLI Reference -> **The web UI is the recommended interface for daily operations** — launching +> **The web UI is the recommended interface for daily operations**: launching > cases, monitoring runs, restarting from checkpoint, inspecting logs and > residuals, cleaning up. Start it with `csauto serve RUNS`. > > Use the CLI when you need: -> - **Setup steps** (`prepare`, `doctor`) — no UI equivalent +> - **Setup steps** (`prepare`, `doctor`): no UI equivalent > - **Scripting and automation** in CI/CD pipelines or shell scripts > - **Data export** to CSV/SVG (`residuals`, `perf`) > - **Terminal-only** access (no SSH tunnel set up) @@ -26,22 +26,26 @@ csauto --config /path/to/csauto.toml ... If `--config` is omitted, config discovery follows the order in [config.md](./config.md). +Commands that take a `` use the solver recorded in +`/campaign.json` by `prepare`, whatever directory they run from. + --- ## Command overview | Command | What it does | UI equivalent | |---|---|---| -| `prepare` | Generate case folders from DOE + template | — (CLI only) | -| `doctor` | Run pre-launch environment checks | — (CLI only) | -| `serve` | Start the primary FastAPI web UI + HTTP API server | — (starts the UI) | -| `run` | Launch selected or all prepared cases | Status panel → Run Selected | +| `prepare` | Generate case folders from DOE + template | CLI only | +| `doe` | Generate a `doe.csv` from a parameter spec (see [doe-format.md](./doe-format.md#generating-doecsv-with-csauto-doe)) | CLI only | +| `doctor` | Run pre-launch environment checks | CLI only | +| `serve` | Start the primary FastAPI web UI + HTTP API server | Starts the UI | +| `run` | Launch the selected cases, or every case not `RUNNING` | Status panel → Run | | `status` | Refresh and print current case states | Status panel (auto-refreshes) | | `tail` | Stream a case log file (like `tail -f`) | Log Tail panel | -| `control` | Steer a running case (stop/extend/checkpoint/flush) without killing it | Status panel → Stop, More ▾ menu (Extend/Checkpoint/Flush) | +| `control` | Send one of the solver's live control actions to a running case (code_saturne: stop, extend, checkpoint, flush) | Status panel → Control menu | | `residuals` | Export residuals data and/or SVG plot | Residuals Plot panel | -| `perf` | Extract performance metrics from logs | Timing Snapshot panel | -| `cleanup` | Prune old data and truncate heavy logs | Status panel → Clean Selected | +| `perf` | Export the solver's timing metrics | Timing Snapshot panel | +| `cleanup` | Prune old run folders and truncate heavy logs | Status panel → Clean | --- @@ -58,25 +62,32 @@ csauto prepare Arguments: - ``: parameter table with header row (see [doe-format.md](./doe-format.md)) -- ``: base case root containing `setup.xml` (at root or under `DATA/`) -- ``: destination root where `caseXXXX/` folders are created +- ``: base case root containing the solver's setup file (code_saturne: + `setup.xml`, at the root or under `DATA/`; code_aster: a single `.export` file) +- ``: destination root where the case folders are created (`case0001/`, + ..., or the DOE's `case_id` values) Options: -- `--mesh-mode {copy,symlink}`: how to place the shared `MESH`/`POST` dirs (siblings of - ``) into ``. Defaults to `mesh_mode` in `csauto.toml` - (itself defaulting to `symlink`; container runtimes bind-mount the symlink - targets automatically). See [concepts.md](./concepts.md#shared-meshpost-directories) - for the tradeoffs. +- `--mesh-mode {copy,symlink}`: how to place the solver's shared dirs (siblings of + ``: `MESH` and `POST` for code_saturne, `MESH` for code_aster) into + ``. Defaults to `mesh_mode` in `csauto.toml` (itself defaulting to + `symlink`; container runtimes bind-mount the symlink targets automatically). See + [concepts.md](./concepts.md#shared-meshpost-directories) for the tradeoffs. - `--strict`: fail (instead of warn) when a DOE column matches nothing in the - template — the same silent-mismatch failure mode `csauto doe` checks for at + template, the same silent-mismatch failure mode `csauto doe` checks for at generation time. +`prepare` records the solver (`solver` in `csauto.toml`, `code_saturne` by +default) in `/campaign.json`, and refuses to add cases to a folder +prepared for another solver. + Generated files: ``` / ├── registry.json +├── campaign.json ← the solver of this campaign ├── case0001/ │ ├── DATA/setup.xml ← rendered from template │ └── doe_row.csv ← DOE values used for this case @@ -93,20 +104,26 @@ csauto prepare /data/doe.csv /data/TEMPLATE /data/RUNS Common errors: -- `setup.xml` missing from template -- placeholder variable not found in DOE header +- the solver's setup file is missing from the template +- a placeholder variable is not found in the DOE header (the error names the file; + write `\{name}` to keep a literal `{name}`) - empty DOE value for an active placeholder -- existing case folder conflicts with the current DOE/template content +- an existing case folder no longer matches the current DOE/template content + (a new column or a template change does this to every case): `prepare` stops there +- `` holds a campaign of another solver --- ## `run` -> **UI equivalent**: Status panel → select cases → **Run Selected** (recommended +> **UI equivalent**: Status panel → select cases → **Run** (recommended > for interactive use). Use this CLI command for scripting or when the UI is not running. -Launches one or more prepared cases. Cases run as background processes (local) or -Slurm batch jobs depending on the configuration. +Launches cases: by default every case of the campaign that is not `RUNNING`, +finished ones included (a code_saturne case gets a new run, a code_aster case +replaces its results). `--case` limits the launch to the named cases, `--resume` +to `FAILED` ones. Cases run as background processes (local) or Slurm batch jobs +depending on the configuration. ```bash csauto run --n --nt [options] @@ -114,18 +131,18 @@ csauto run --n --nt [options] Required arguments: -- ``: root folder containing the `caseXXXX/` directories +- ``: the campaign folder created by `prepare` - `--n N`: number of MPI ranks per case (must be `> 0`) - `--nt N`: number of OpenMP threads per MPI rank (must be `> 0`) Options: -- `--max-parallel N`: maximum number of cases running simultaneously (default: from config) -- `--case CASE_ID`: launch only specific cases — repeatable +- `--max-parallel N`: maximum number of the campaign's cases running at the same time, those already running included (default: `max_parallel` from config, which defaults to `1`) +- `--case CASE_ID`: launch only specific cases (repeatable) - `--resume`: launch only cases currently in `FAILED` status - `--runtime auto|docker|singularity|native`: override runtime from config -- `--docker-image IMAGE`: override Docker image -- `--saturne-bin PATH`: override native code_saturne binary path +- `--docker-image IMAGE`: Docker image (default: `docker_image` from config, else the solver's own image) +- `--saturne-bin PATH`: the native solver executable (`code_saturne`, or `run_aster` for code_aster) - `--singularity-image PATH`: override Singularity image - `--singularity-bin PATH`: override Singularity/Apptainer binary path - `--no-doctor`: skip pre-launch environment checks @@ -133,7 +150,16 @@ Options: Notes: - Restart (from checkpoint) is available only via the web UI or API, not via this CLI command -- Slurm submission is controlled by `use_slurm` in `csauto.toml` or `CSAUTO_USE_SLURM` +- Cases already `RUNNING` are skipped +- The solver starts inside the case folder, whatever the runtime. Docker runs + use `docker run --rm` with the campaign folder mounted at `/csauto` +- How `--n` and `--nt` are used depends on the solver: code_saturne receives + them as `-n` and `--nt`; code_aster runs `.csauto.export`, a copy of the case's + export with `mpi_nbcpu` set from `--n` and `ncpus` from `--nt` +- A case the solver refuses to launch keeps its status, the other cases still + launch, and the command ends with an error naming the refused cases +- Slurm submission is controlled by `use_slurm` in `csauto.toml` or `CSAUTO_USE_SLURM`. + `sbatch --wrap` jobs request `--ntasks` from `--n` and `--cpus-per-task` from `--nt` Examples: @@ -182,7 +208,8 @@ csauto status RUNS > a work session and keep it running. See [web-ui.md](./web-ui.md) for the full > UI guide. -Starts the primary FastAPI web UI and HTTP REST API server. +Starts the primary FastAPI web UI and HTTP REST API server. The dashboard shows +the panels and actions of the campaign's solver. ```bash csauto serve [--host 127.0.0.1] [--port 8000] [--token TOKEN] [--no-doctor] @@ -213,8 +240,8 @@ See [web-ui.md](./web-ui.md) for the full UI guide. ## `tail` -> **UI equivalent**: **Log Tail** panel in the web UI — same file selection, -> live streaming, and severity filtering, directly in the browser. +> **UI equivalent**: **Log Tail** panel in the web UI, with the same file +> selection, live streaming, and severity filtering, directly in the browser. > Use this CLI command for quick terminal access or when the UI is not running. Streams the end of a case log file, like `tail -f`. @@ -226,79 +253,87 @@ csauto tail --case CASE_ID [--file FILE] [-n LINES] [--no-follow] Options: - `--case CASE_ID`: case to inspect (required) -- `--file FILE`: log file to read. Common values: - - `listing` — main solver log (default) - - `run_solver.log` — solver launch log - - `csauto.stdout` — csauto launch output (useful if the process didn't start) - - `csauto.stderr` — csauto launch errors -- `-n N`: initial number of lines to display (default: `20`) +- `--file FILE`: log file to read. By default, the first file the Log Tail + offers for the case, usually the solver's main log (`run_solver.log` for + code_saturne, `csauto.stdout` for code_aster). Other common values: + - `csauto.stdout`: the run command's output (useful if the solver didn't start) + - `csauto.stderr`: the run command's errors + - `listing`: code_saturne's main log on older versions + - any path relative to the case +- `-n N`: number of lines shown at start (default: `20`) - `--no-follow`: print once and exit instead of following new lines Examples: ```bash -# Follow main solver log, show last 200 lines -csauto tail RUNS --case case0001 --file listing -n 200 +# Follow the solver's main log +csauto tail RUNS --case case0001 -# One-shot read of launch log -csauto tail RUNS --case case0001 --file csauto.stdout -n 50 --no-follow +# One-shot read of the launch log +csauto tail RUNS --case case0001 --file csauto.stdout --no-follow ``` --- ## `control` -> **UI equivalent**: Status panel → **Stop** button, plus a **More ▾** menu -> with Extend, Checkpoint, and Flush. +> **UI equivalent**: Status panel → **Control** menu, which lists the same actions. + +Sends one of the solver's live control actions to a running case. The actions +depend on the solver, and `csauto doctor RUNS` lists them with the value each +one takes: -Sends a live steering directive to a running case by dropping a -`control_file` into its active `RESU//` directory, which code_saturne -polls once per time step. This is the non-destructive alternative to killing -the process: the solver finishes its current step cleanly instead of being -interrupted mid-iteration. +``` +[OK] solver code_saturne: control actions stop, extend , checkpoint, flush +``` ```bash -csauto control [--stop | --extend N | --checkpoint | --flush] +csauto control [value] ``` -Exactly one action is required: +For code_saturne, csauto drops a `control_file` into the case's active +`RESU//` directory, which code_saturne polls once per time step. This is +the non-destructive alternative to killing the process: the solver finishes its +current step cleanly instead of being interrupted mid-iteration. Its actions: -- `--stop`: graceful stop — finish the current time step, write a checkpoint, +- `stop`: graceful stop. Finish the current time step, write a checkpoint, and exit. No restart is needed afterwards; the run is already at a consistent state. -- `--extend N`: raise the case's configured iteration limit (`nt_max`, as - code_saturne itself reports it — not csauto's own `--nt`/OpenMP thread count) +- `extend N`: raise the case's configured iteration limit (`nt_max`, as + code_saturne itself reports it, not csauto's own `--nt`/OpenMP thread count) by `N`, so it keeps running past a limit it's about to hit instead of stopping. csauto reads the actual limit from the solver's own logs (`setup.log`, or a prior extend's echo in `run_solver.log`/`listing`), so repeated extends compound correctly. Falls back to the case's current iteration if the configured limit can't be read yet (e.g. right after launch, before `setup.log` is written), or `0` if that isn't available either. -- `--checkpoint`: request a checkpoint at the next time step, without +- `checkpoint`: request a checkpoint at the next time step, without stopping the run. -- `--flush`: flush logs and time plots at the next time step. +- `flush`: flush logs and time plots at the next time step. -The case must be `RUNNING` for any of these to apply. Each action is logged -to the case's `.csauto.history.jsonl`. +The case must be `RUNNING`. An action that takes a value refuses to run +without one, and the others refuse a value. Each action is logged to the +case's `.csauto.history.jsonl`. code_aster has no live control actions. Examples: ```bash -csauto control RUNS case0007 --stop -csauto control RUNS case0007 --extend 500 -csauto control RUNS case0007 --checkpoint -csauto control RUNS case0007 --flush +csauto control RUNS case0007 stop +csauto control RUNS case0007 extend 500 +csauto control RUNS case0007 checkpoint +csauto control RUNS case0007 flush ``` --- ## `residuals` -> **UI equivalent**: **Residuals Plot** panel — interactive case selection, +> **UI equivalent**: **Residuals Plot** panel, with interactive case selection, > quantity picker, and live chart. Use this CLI command when you need to export > data to CSV or SVG for reports or external processing. Collects residual data from one or more cases and optionally renders an SVG plot. +Residuals are available for solvers that provide them (code_saturne). ```bash csauto residuals --case CASE_ID [--case CASE_ID ...] [--out FILE] [--plot FILE] [--columns COL ...] @@ -306,13 +341,14 @@ csauto residuals --case CASE_ID [--case CASE_ID ...] [--out FILE] [-- Options: -- `--case CASE_ID`: case to include — required, repeatable +- `--case CASE_ID`: case to include (required, repeatable) - `--out FILE`: write merged CSV to this path (stdout if omitted) - `--plot FILE`: render residuals as SVG to this path - `--columns COL [COL ...]`: limit SVG plot to these residual quantities The output CSV has one row per iteration, with columns for `case_id`, -`iteration`, and each residual quantity found in `residuals.csv`. +`iteration`, and each residual quantity found in code_saturne's `residuals.csv` +(or parsed from `run_solver.log` when that file is missing). Examples: @@ -329,10 +365,13 @@ csauto residuals RUNS --case case0001 --case case0002 \ ## `perf` -> **UI equivalent**: **Timing Snapshot** panel — select a case and click **Load**. +> **UI equivalent**: **Timing Snapshot** panel, which lists the timings of every +> case that has a timing log and refreshes on its own (narrow with **Cases**, +> export with **Download as CSV**). > Use this CLI command to export metrics to CSV for batch comparison or reporting. -Extracts timing and performance metrics from `performance.log` for one or more cases. +Extracts timing metrics from the solver's timing log (code_saturne: +`performance.log`) for one or more cases. ```bash csauto perf --case CASE_ID [--case CASE_ID ...] [--out FILE] @@ -340,11 +379,12 @@ csauto perf --case CASE_ID [--case CASE_ID ...] [--out FILE] Options: -- `--case CASE_ID`: case to include — required, repeatable +- `--case CASE_ID`: case to include (required, repeatable) - `--out FILE`: write CSV to this path (stdout if omitted) -The output CSV includes: elapsed time, number of processes, number of threads, -solver phase timings (initialization, time loop, I/O), and derived metrics. +The output CSV has `case_id` plus the solver's timing columns, the same as the +Timing Snapshot panel. For code_saturne: `elapsed_time`, `io_time`, +`linear_solver_time`, `gradients_time`, `balances_time`, `mpi_ranks`, `threads`. Examples: @@ -369,22 +409,31 @@ csauto doctor Checks performed: - `RUNS/` directory is writable -- At least one `case*` directory exists -- `setup.xml` is present in each case -- Runtime binary / image is accessible (reads `csauto.toml` — native binary, Singularity image, or Docker) -- X11 display availability (for GUI launch) +- At least one case exists (a folder listed in `registry.json` or holding a `doe_row.csv`) +- The solver's setup file is present in each case (`setup.xml`, or the `.export` for code_aster) +- What the solver offers: dashboard panels, capabilities, live control actions, and supported runtimes +- The runtime is usable (reads `csauto.toml`): the native executable, the Singularity + binary and image, or the `docker` command (the Docker image itself is not checked) +- X11 display availability (for GUI launch, code_saturne) +- Web dependencies (`fastapi`, `uvicorn`, `pydantic`) for `csauto serve` -Each check prints `[OK]` or `[FAIL]`. Fix all failures before running. +Each check prints `[OK]`, `[WARN]` or `[FAIL]`. Fix all failures before running. --- ## `cleanup` -> **UI equivalent**: Status panel → select cases → **Clean Selected**. The UI -> popup lets you choose interactively what to keep or delete. +> **UI equivalent**: Status panel → select cases → **Clean**. The UI +> popup lets you choose interactively which runs to keep or delete; Clean also +> cuts the solver's logs over 50 MB down to their last 50 MB and removes `.csauto.cid`. > Use this CLI command for scripting, scheduled cleanups, or disk-space emergencies. -Removes old RESU directories and truncates oversized logs. +Removes old run folders and truncates oversized logs. `--prune-resu` deletes +only the run folders the solver reports: each `RESU/` for code_saturne, +`RESU` itself for code_aster (one run per case). Statuses are refreshed first, +as `csauto status` does, so a run that has ended is not taken for a running +one; `RUNNING` and `PENDING` cases are then skipped and listed. Unlike the +dashboard's Clean, this command never resets a case to `PREPARED`. ```bash csauto cleanup [options] @@ -392,12 +441,12 @@ csauto cleanup [options] Options: -- `--prune-resu`: enable removal of old RESU run directories -- `--keep-last N`: keep the N most recent RESU dirs per case (default: `1`) -- `--max-log-mb X`: truncate log files larger than X MB +- `--prune-resu`: enable removal of old run folders +- `--keep-last N`: keep the N most recent run folders per case (default: `1`, `0` to delete all) +- `--max-log-mb X`: truncate the solver's log files larger than X MB, keeping their end - `--clear-cid`: remove `.csauto.cid` process tracking files - `--clear-pyc`: remove `__pycache__` directories under case folders -- `--dry-run`: preview what would be deleted — no actual changes +- `--dry-run`: preview what would be deleted, without any actual change If no cleanup option is provided, the command exits with an informational message and makes no changes. @@ -411,12 +460,12 @@ csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 --dry-run Examples: ```bash -# Remove all but the latest RESU run, truncate logs > 100 MB +# Remove all but the latest run, truncate logs > 100 MB csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 # Same, but preview first csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 --dry-run -# Remove all RESU history (keep none) +# Remove every run (keep none) csauto cleanup RUNS --prune-resu --keep-last 0 ``` diff --git a/docs/concepts.md b/docs/concepts.md index 06d19bb..6330a68 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -7,9 +7,9 @@ Reading it once will make everything else click. When you run a code_saturne simulation study, you typically need to explore many parameter combinations: different inlet velocities, turbulence models, mesh -refinements, boundary conditions. Setting up each case by hand — copying -directories, editing XML files, launching processes, tracking outcomes — does not -scale beyond a handful of cases. +refinements, boundary conditions. Setting up each case by hand (copying +directories, editing XML files, launching processes, tracking outcomes) does not +scale beyond a handful of cases. The same holds for a code_aster study. csauto automates this entire loop: @@ -36,7 +36,7 @@ monitors everything. ## Key concepts -### DOE — Design of Experiments table +### DOE: Design of Experiments table Your parameter space is described in a CSV file called the **DOE** (Design of Experiments). Each row is one simulation case. Each column is one input parameter. @@ -54,11 +54,21 @@ and generates one case directory per row. The `case_id` column is optional. If absent, csauto auto-generates IDs: `case0001`, `case0002`, etc. +### Solver + +The **solver** is set by `solver` in `csauto.toml`: `code_saturne` (the default) +or `code_aster`. csauto knows each solver through an adapter that says how to +start it, where its files are, and how to read its output, so the dashboard +shows what that solver offers. `csauto prepare` records the solver in +`RUNS/campaign.json`; every later command on that `RUNS` folder uses it, from +any directory. + ### Template The **template** is your reference case directory. It contains all the files -needed to run one code_saturne simulation: `setup.xml`, optionally `run.cfg`, -optionally user source files under `SRC/`. +needed to run one simulation. For code_saturne: `setup.xml`, optionally +`run.cfg`, optionally user source files under `SRC/`. For code_aster: one +`.export` file and the command files it lists. Instead of hard-coded values, the template uses **placeholders**: @@ -79,21 +89,25 @@ row, evaluates IF conditions, and writes the result into each case directory. ### Placeholder syntax -- `{column_name}` — replaced with the DOE value for that column -- `...` — block included only if condition is true -- `...` — block included only if condition is false -- `...` — block included only if column value is truthy +- `{column_name}`: replaced with the DOE value for that column +- `...`: block included only if condition is true +- `...`: block included only if the value differs +- `...`: block included only if column value is truthy +- `\{name}`: rendered as a literal `{name}`, for braces that belong to the input + language (Python f-strings or sets in a code_aster `.comm` file); see + [doe-format.md](./doe-format.md#placeholder-syntax) Shell variables like `${HOME}` are left untouched. ### Case directory -A **case** (`caseXXXX`) is a fully resolved copy of your template for one DOE -row. After `csauto prepare`, each case contains: +A **case** (`caseXXXX`, or the DOE's `case_id`) is a fully resolved copy of your +template for one DOE row. After `csauto prepare`, each case contains: ``` RUNS/ ├── registry.json ← global state for all cases +├── campaign.json ← the solver of this campaign ├── case0001/ │ ├── DATA/ │ │ └── setup.xml ← rendered from template + DOE row @@ -104,7 +118,8 @@ RUNS/ ``` The `doe_row.csv` file inside each case records exactly which parameter values -were used — useful for traceability. +were used, which is useful for traceability. With the registry, it is also how +csauto recognizes a case folder. ### RUNS directory @@ -124,20 +139,22 @@ the browser: launching, monitoring, restarting, cleaning. The **registry** (`RUNS/registry.json`) is csauto's persistent state. It stores the current status, process IDs, Slurm job IDs, iteration count, and metadata for -every case. You never edit it manually — csauto maintains it automatically. +every case. You never edit it manually: csauto maintains it automatically. ### Runtime -A **runtime** is the execution backend csauto uses to actually run code_saturne: +A **runtime** is the execution backend csauto uses to actually run the solver: | Runtime | When to use | |---|---| -| `native` | code_saturne binary installed directly on the machine | -| `docker` | code_saturne inside a Docker container | -| `singularity` | code_saturne inside a Singularity/Apptainer image (common on HPC) | -| `auto` | csauto detects the best available option | +| `native` | the solver installed directly on the machine (`code_saturne`, or `run_aster` for code_aster) | +| `docker` | the solver inside a Docker container (the image defaults to the solver's own) | +| `singularity` | the solver inside a Singularity/Apptainer image (common on HPC) | +| `auto` | csauto picks the first available among the runtimes the solver supports | -The runtime is set in `csauto.toml` or via CLI flags. +The runtime is set in `csauto.toml` or via CLI flags. Whatever the runtime, the +solver starts inside the case folder; containers mount the campaign folder at +`/csauto`. ### RESU directory @@ -147,8 +164,7 @@ when a run starts: ``` RUNS/case0001/RESU/ └── 20260308-1413/ ← one directory per run attempt - ├── listing ← main solver log - ├── run_solver.log + ├── run_solver.log ← main solver log (listing on older versions) ├── residuals.csv ├── performance.log ├── checkpoint/ ← restart data @@ -159,14 +175,18 @@ RUNS/case0001/RESU/ Each run attempt gets a timestamped directory. If you restart a case, a new `RESU//` directory is created alongside the previous ones. +code_aster writes its results straight into `RESU/` (the paths its `.export` +gives, such as `RESU/myresults.rmed`), so each case holds one run and a new run +replaces it. + ### Checkpoint and restart A **checkpoint** is the restart data saved by code_saturne at the end of (or during) a run. It lives at `RESU//checkpoint/`. csauto resolves the latest checkpoint automatically. From the web UI, select -the case, click **Restart Selected**, choose a stop criterion, and submit — -no need to know the checkpoint path. +the case, click **Restart**, choose a stop criterion, and submit: no need to +know the checkpoint path. Restart is available for code_saturne. --- @@ -178,15 +198,19 @@ A case moves through these states: [prepare] │ ▼ -PREPARED ──► [run] ──► RUNNING ──► DONE - │ - └──► FAILED +PREPARED ──► [run] ──► PENDING ──► RUNNING ──► DONE + │ + └──► FAILED ``` -- **PREPARED**: case directory exists, never launched -- **RUNNING**: active process or Slurm job -- **DONE**: completed successfully (log signature detected) -- **FAILED**: abnormal termination or error detected +- **PREPARED**: case directory exists, not launched yet (or the dashboard's Clean deleted all its runs) +- **PENDING**: next in line, waiting for a free launch slot (Max Parallel); the + other cases of the batch keep their status until their turn +- **RUNNING**: active process or Slurm job. A case stays `RUNNING` until its + process or job is gone, even after its log prints a verdict +- **DONE**: the run ended and the solver reported success, or, when its logs give + no verdict, its command exited with status 0 +- **FAILED**: the solver reported a failure, or, with no verdict, its command did not exit with status 0 For finished cases, you can set a convergence label (`Converged` / `Not converged`) from the UI context menu. @@ -195,12 +219,13 @@ For finished cases, you can set a convergence label (`Converged` / ## Shared MESH/POST directories -`prepare` places one `MESH/` and one `POST/` folder directly under `RUNS/` (not -per-case) so every case can reference the mesh with a relative `../MESH` path. -How that folder gets there is controlled by `mesh_mode` in `csauto.toml` (or -`--mesh-mode` on `csauto prepare`): +`prepare` places the solver's shared folders (code_saturne: `MESH/` and `POST/`; +code_aster: `MESH/`) directly under `RUNS/` (not per-case) so every case can +reference the mesh with a relative `../MESH` path (a code_aster `.export` lists +`../MESH/`). How that folder gets there is controlled by `mesh_mode` in +`csauto.toml` (or `--mesh-mode` on `csauto prepare`): -- `symlink` (default) — `RUNS/MESH` and `RUNS/POST` become symlinks to the +- `symlink` (default): `RUNS/MESH` and `RUNS/POST` become symlinks to the original directories instead of copies, so large meshes (`.med` files can be gigabytes) are never duplicated per study. Container runtimes handle this transparently: `csauto run` bind-mounts each symlink target into the @@ -208,9 +233,9 @@ How that folder gets there is controlled by `mesh_mode` in `csauto.toml` (or writable), so the symlinks stored in `RUNS/` resolve identically on both sides. A broken symlink (target deleted or moved) is still detected up front and reported before launching. -- `copy` — the template's sibling `MESH/`/`POST/` dirs are physically copied - into `RUNS/`. Uses more disk, but makes `RUNS/` fully self-contained — useful - when the runs directory is moved or archived independently of the study. +- `copy`: the template's sibling `MESH/`/`POST/` dirs are physically copied + into `RUNS/`. Uses more disk, but makes `RUNS/` fully self-contained, which is + useful when the runs directory is moved or archived independently of the study. - On Windows, creating a symlink may require developer mode or elevated privileges; `prepare` falls back to copying (with a warning) if the symlink cannot be created. @@ -223,7 +248,7 @@ On HPC clusters, csauto submits each case as a Slurm job via `sbatch` instead of launching a local background process. The job ID is tracked in the registry. Status refreshes use `squeue` to detect whether the job is still running. -Local and Slurm modes are configured in `csauto.toml` — see [config.md](./config.md). +Local and Slurm modes are configured in `csauto.toml`; see [config.md](./config.md). --- @@ -238,9 +263,10 @@ my-campaign/ │ └── setup.xml └── RUNS/ ← generated cases (created by prepare) ├── registry.json + ├── campaign.json ├── case0001/ └── case0002/ ``` Keep `RUNS/` out of version control (add it to `.gitignore`). Track -`doe.csv` and `TEMPLATE/` — they are your actual inputs. +`doe.csv` and `TEMPLATE/`: they are your actual inputs. diff --git a/docs/config.md b/docs/config.md index 20b8c60..c6c5bab 100644 --- a/docs/config.md +++ b/docs/config.md @@ -24,7 +24,7 @@ If no file is found, all default values are used. runtime = "native" saturne_bin = "/opt/code_saturne/bin/code_saturne" -# No Slurm, run up to 2 cases at the same time +# No Slurm; csauto run launches at most 2 cases at a time use_slurm = false max_parallel = 2 @@ -46,7 +46,7 @@ use_slurm = true # MPI options injected via CS_MPIEXEC_OPTIONS for each sbatch job mpi_exec_options = "--mca btl vader,self,tcp --bind-to core" -# Allow up to 20 jobs in the queue simultaneously +# csauto run keeps at most 20 jobs submitted (queued or running) at a time max_parallel = 20 # Serve UI on login node, tunnel with: ssh -L 8000:127.0.0.1:8000 login-node @@ -68,16 +68,17 @@ host = "127.0.0.1" port = 8000 ``` -With `use_slurm = true`, `csauto` submits a dedicated batch script for the -Singularity runtime. The job follows the same pattern as a manual HPC launch: +For code_saturne with `use_slurm = true`, `csauto` submits a dedicated batch +script for the Singularity runtime (`.csauto.slurm.sh` in the case folder). The +job follows the same pattern as a manual HPC launch: `code_saturne run --stage --initialize`, then `srun ./cs_solver --mpi`, then -`code_saturne run --finalize`. +`code_saturne run --finalize`. Other runs are submitted with `sbatch --wrap`. ### Docker container ```toml runtime = "docker" -docker_image = "simvia/code_saturne" +docker_image = "simvia/code_saturne" # optional: the solver's own image by default use_slurm = false max_parallel = 2 @@ -86,6 +87,38 @@ host = "127.0.0.1" port = 8000 ``` +Containers mount the campaign folder at `/csauto` and start the solver inside the +case folder (`docker run --rm ...`). Any image works: + +- an image whose ENTRYPOINT is the solver, or a script that sets up the + environment and starts it (modules, spack, conda, a switch to another user), + gets only the solver's arguments, as before; +- an image whose ENTRYPOINT runs the command it is given (`tini --`, + `exec "$@"` scripts) gets the full solver command; +- an image without ENTRYPOINT gets the solver as `--entrypoint`, so it must be + on the image's `PATH`. + +csauto tells the first two apart by starting the image once with `true` (one +short container per image, the first time a csauto process launches it). + +### code_aster + +```toml +solver = "code_aster" +runtime = "docker" # docker_image defaults to simvia/code_aster:17.4.0 + +max_parallel = 2 +``` + +For the `native` runtime, `run_aster` must be in `PATH`, or `saturne_bin` must +point to it. + +In containers, csauto starts `run_aster` with `bash`, after sourcing +`/opt/activate.sh` when the image has it (the `simvia/code_aster` images do). A +custom image needs `bash`, and `run_aster` on its `PATH` once that file is +sourced (or from the start, when the image has no such file). An image whose +ENTRYPOINT starts `run_aster` itself only gets its arguments. + ### Remote access with authentication ```toml @@ -109,16 +142,16 @@ token = "your-secret-token" | Key | Default | Description | |---|---|---| -| `solver` | `code_saturne` | Solver adapter used for command building, output parsing, and file conventions (`code_saturne`, or `stub` for testing; see [architecture.md](./architecture.md#solver-adapter-boundary)) | +| `solver` | `code_saturne` | The campaign's solver: `code_saturne`, `code_aster`, or `stub` (a fake solver for tests). `csauto prepare` records it in `RUNS/campaign.json` (see [architecture.md](./architecture.md#solver-adapter-boundary)) | | `runtime` | `auto` | Execution backend: `auto`, `native`, `docker`, or `singularity` | -| `saturne_bin` | (auto-detected) | Path to the `code_saturne` binary for `native` runtime | -| `docker_image` | `simvia/code_saturne` | Docker image name for `docker` runtime | +| `saturne_bin` | (auto-detected) | Path to the native solver executable (`code_saturne`, or `run_aster` for code_aster) for `native` runtime | +| `docker_image` | (the solver's own image) | Docker image name for `docker` runtime; defaults to `simvia/code_saturne` for code_saturne, `simvia/code_aster:17.4.0` for code_aster. csauto keeps the image's own ENTRYPOINT (see [Docker container](#docker-container)) | | `singularity_image` | (none) | Path or URI to `.sif` image for `singularity` runtime | | `singularity_bin` | (auto-detected) | Path to `apptainer` or `singularity` binary | | `use_slurm` | (auto-detected) | `true` to force Slurm submission, `false` to force local | -| `mpi_exec_options` | (none) | Options injected as `CS_MPIEXEC_OPTIONS` in Slurm jobs | -| `max_parallel` | `1` | Maximum simultaneous case launches | -| `mesh_mode` | `symlink` | How `prepare` places the shared `MESH`/`POST` dirs into `RUNS/`: `symlink` or `copy` (see [concepts.md](./concepts.md#shared-meshpost-directories)) | +| `mpi_exec_options` | (none) | MPI options for Slurm jobs, passed to code_saturne as `CS_MPIEXEC_OPTIONS` (code_aster ignores it) | +| `max_parallel` | `1` | Default of `csauto run --max-parallel`. The web UI does not read it: set Max Parallel in the Run or Restart dialog (left empty, every selected case starts at once) | +| `mesh_mode` | `symlink` | How `prepare` places the solver's shared dirs (`MESH`, plus `POST` for code_saturne) into `RUNS/`: `symlink` or `copy` (see [concepts.md](./concepts.md#shared-meshpost-directories)) | | `host` | `127.0.0.1` | Bind address for `serve` | | `port` | `8000` | Bind port for `serve` | | `[api].token` | (none) | API authentication token | @@ -139,8 +172,13 @@ token = "your-secret-token" **Runtime**: CLI flags (`--runtime`, `--saturne-bin`, etc.) override the TOML values. +**Solver**: commands on an existing campaign use the solver recorded in +`RUNS/campaign.json` by `prepare`, whatever directory they run from; a different +`solver` in `csauto.toml` is ignored with a warning. `prepare` refuses to add +cases to a folder prepared for another solver. + **Slurm detection order**: -1. `use_slurm` in `csauto.toml` — if set, this is authoritative +1. `use_slurm` in `csauto.toml`: if set, this is authoritative 2. `CSAUTO_USE_SLURM` environment variable 3. Auto-detection: checks if `sbatch` is in `PATH` and Slurm environment variables are set @@ -150,23 +188,28 @@ token = "your-secret-token" ## Auto runtime resolution -When `runtime = "auto"`, csauto tries backends in this order: +When `runtime = "auto"`, csauto tries backends in this order, among the +runtimes the solver supports (code_saturne and code_aster support all three): 1. Explicit `saturne_bin` set → use `native` 2. Explicit `singularity_image` set → use `singularity` 3. `docker` command available → use `docker` -4. `code_saturne` in `PATH` → use `native` +4. The solver's executable (`code_saturne`, `run_aster`) in `PATH` → use `native` 5. None found → error +Asking explicitly for a runtime the solver does not support is refused before +anything starts. + --- ## `mpi_exec_options` and Slurm -When Slurm submission is active and `mpi_exec_options` is set, the value is -injected as: +When Slurm submission is active and `mpi_exec_options` is set, code_saturne +receives the value as: ```bash CS_MPIEXEC_OPTIONS="" ``` -If not set, no `CS_MPIEXEC_OPTIONS` is injected. +With a container runtime, the variable is set inside the container instead. If +not set, no `CS_MPIEXEC_OPTIONS` is injected. code_aster ignores this option. diff --git a/docs/doe-format.md b/docs/doe-format.md index bcf292e..2a4dcc5 100644 --- a/docs/doe-format.md +++ b/docs/doe-format.md @@ -85,7 +85,7 @@ digits, `.`, `_`, `-`) and `case_id` is reserved. For `lhs`/`sobol`/`ccd`, continuous parameters are sampled using the chosen method, and discrete parameters are crossed with every sample (outer -product) — e.g. 10 LHS samples over one continuous parameter crossed with a +product): e.g. 10 LHS samples over one continuous parameter crossed with a 2-level discrete parameter yields 20 rows. Continuous values are rounded to `--round` digits (default 6). Use `--seed` @@ -98,7 +98,7 @@ CSV). Pass `--force` to overwrite an existing output file. placeholders and `` condition variables (from `./TEMPLATE` by default, or the directory given with `--template DIR`): -- a spec parameter matching nothing in the template is an **error** — a typo +- a spec parameter matching nothing in the template is an **error**: a typo like `u_inlett` vs `{u_inlet}` would otherwise generate a study that runs successfully with the template's hardcoded value, never varying the intended parameter; @@ -118,9 +118,12 @@ error. - If absent, IDs are auto-generated in sequence: `case0001`, `case0002`, ... - If provided: - Must be non-empty - - Allowed characters: letters, digits, `.`, `_`, `-` + - Allowed characters: letters, digits, `.`, `_`, `-`; the first one must be a + letter or a digit - `/` and `\` are forbidden - Example valid IDs: `case0001`, `run-A`, `mesh.fine` +- Every command works with any valid ID: csauto finds cases from its registry + and their `doe_row.csv`, not from their names --- @@ -133,10 +136,19 @@ Write `{column_name}` anywhere in your template files: ``` Rules: -- Every placeholder must have a matching column in the DOE header +- Every placeholder must have a matching column in the DOE header (the error + names the file that uses it) - Active placeholders cannot have an empty value - Shell-style `${VAR}` patterns are ignored (not processed) -- Placeholders work in `setup.xml`, `run.cfg`, and `SRC/*.cpp` user files +- Write `\{name}` to keep a literal `{name}`, when braces belong to the input + language (Python f-strings or sets in a code_aster `.comm` file, C/C++ code). + csauto removes the backslash when it generates the cases +- Placeholders work in every text file of the template: the solver's setup file + (`setup.xml`, or the code_aster `.export`), `run.cfg`, user sources such as + `SRC/*.cpp`, code_aster `.comm` files. Binary files, hidden files and editor + backup files are copied without rendering +- Rendered files keep their bytes: text in another encoding than UTF-8 (a + comment in Latin-1, say) comes out unchanged --- @@ -154,8 +166,8 @@ Supported forms: | Syntax | Included when | |---|---| -| `` | value is non-empty and not `false`/`0` | -| `` | value matches exactly | +| `` | value is not empty, `0`, `false`, `no`, `off` or `none` (any case) | +| `` or `` | value matches exactly | | `` | value does not match | The condition variable (`key`) must also have a matching column in the DOE header. @@ -164,11 +176,11 @@ The condition variable (`key`) must also have a matching column in the DOE heade ## Empty values and unused columns -- **Empty value for an active placeholder**: error — csauto stops with +- **Empty value for an active placeholder**: error, csauto stops with `Missing placeholder values for caseXXXX` - **Unused column** (no matching placeholder or IF condition): allowed, reported as a warning -- **Empty value inside an inactive IF block**: allowed — the block is skipped +- **Empty value inside an inactive IF block**: allowed, the block is skipped --- @@ -183,7 +195,7 @@ case0004,15.0,1.2,1.8e-5,k-omega,true,DOM ``` Notes: -- `radiation_model` is empty for `case0001` and `case0002` — this is fine because +- `radiation_model` is empty for `case0001` and `case0002`: this is fine because the template wraps `{radiation_model}` inside `` - The IF block is skipped entirely for those cases, so the empty value is never activated diff --git a/docs/formats.md b/docs/formats.md index 4123de2..62fe2fb 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -15,9 +15,9 @@ case0001,0.10,1.20 case0002,0.20,1.30 ``` -## `setup.xml` (template input) +## `setup.xml` (code_saturne template input) -Required template file. +Required template file for code_saturne. Accepted locations: @@ -26,14 +26,22 @@ Accepted locations: Supports placeholders and IF blocks. -## `run.cfg` (optional template input) +## `run.cfg` (optional code_saturne template input) Optional file, can be in template root or under `DATA/`. Also supports placeholders and IF blocks. +## `.export` (code_aster template input) + +Required template file for code_aster: exactly one `*.export` file at the root +of the template. It lists the command files, the mesh and the result files: +reference the shared mesh folder as `../MESH/` and write results under +`RESU/`. Like the `.comm` command files, it supports placeholders and IF blocks. + ## User source files in `SRC/` (optional template input) -Template text files under `SRC/` are also rendered. +Every text file of the template that holds placeholders or IF blocks is rendered, +including user sources under `SRC/`. Example: @@ -41,7 +49,8 @@ Example: Note: -- placeholders are detected using `{name}` syntax, so avoid ambiguous brace patterns in C/C++ code. +- placeholders are detected using `{name}` syntax; write `\{name}` for braces + that must stay as they are in C/C++ code (see [doe-format.md](./doe-format.md#placeholder-syntax)). ## `doe_row.csv` (generated per case) @@ -51,7 +60,25 @@ Generated at: RUNS/case0001/doe_row.csv ``` -Contains the resolved DOE values used for that case. +Contains the resolved DOE values used for that case. With the registry, it is +how csauto recognizes case folders. + +## `campaign.json` (campaign solver) + +Located at: + +```text +RUNS/campaign.json +``` + +Written by `csauto prepare`: + +```json +{ "solver": "code_aster" } +``` + +Every later command on the campaign uses this solver, whatever directory it +runs from. ## `registry.json` (global state) @@ -73,19 +100,36 @@ Stores status and metadata, including: Typical files: ```text -RUNS/case0001/csauto.stdout +RUNS/case0001/csauto.stdout ← output of the run command RUNS/case0001/csauto.stderr RUNS/case0001/.csauto.history.jsonl -RUNS/case0001/.csauto.jobid -RUNS/case0001/RESU//listing -RUNS/case0001/RESU//run_solver.log +RUNS/case0001/.csauto.exitcode ← exit status of the last run command +RUNS/case0001/.csauto.jobid ← Slurm job ID +RUNS/case0001/.csauto.cid ← Docker container ID +RUNS/case0001/.csauto.slurm.sh ← batch script, for solvers that write one +RUNS/case0001/.csauto.export ← code_aster: the export actually run +``` + +`.csauto.exitcode` is written when the run command ends, for local runs and +`sbatch --wrap` jobs. code_saturne writes its Slurm batch script for the +Singularity runtime. code_aster runs `.csauto.export`, a copy of the case's +`.export` with `mpi_nbcpu` and `ncpus` set from `n` and `nt`; the case's own +`.export` is never modified. + +code_saturne writes one folder per run: + +```text +RUNS/case0001/RESU//run_solver.log ← main solver log (listing on older versions) RUNS/case0001/RESU//residuals.csv RUNS/case0001/RESU//performance.log RUNS/case0001/RESU//monitoring/*.csv RUNS/case0001/RESU//profiles/*.csv ``` -## `residuals.csv` +code_aster writes its results straight into `RUNS/case0001/RESU/`, at the paths +its `.export` gives, so a case holds one run. + +## `residuals.csv` (code_saturne) Typical format: @@ -97,13 +141,13 @@ iteration,velocity,pressure If missing, residual fallback parsing from `run_solver.log` may be used. -## Probes and profiles CSV +## Probes and profiles CSV (code_saturne) - probes: `RESU//monitoring/*.csv` - profiles: `RESU//profiles/*.csv` UI labels are derived from physical quantity naming in filenames. -## `performance.log` +## `performance.log` (code_saturne) `csauto` extracts elapsed time, ranks, threads, solver timings, and IO timings from known text patterns. diff --git a/docs/limitations.md b/docs/limitations.md index b228eff..6d22c4a 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,8 +1,17 @@ # Limitations and Best Practices -## Log-based completion detection +## Solver features -`RUNNING -> DONE/FAILED` transitions rely on process/job status and log signatures. +The dashboard shows what the campaign's solver supports. Residuals, probes, +timings, restart, live control and the GUI are available for code_saturne. +code_aster campaigns get Status, Compare, Log Tail and Recent Errors; each +code_aster case holds one run, and a new run replaces its results. + +## Completion detection + +A run ends when its process or Slurm job is gone. Its status then comes from +the solver's verdict in its logs, or, when the logs give none, from the exit +status of its command (`.csauto.exitcode`: 0 means `DONE`). Best practice: keep clean per-run logs and avoid mixing historical logs. @@ -24,7 +33,7 @@ Best practice: ## GUI launch constraints -`open_gui` requires valid `DISPLAY` and X11 access. +`open_gui` (code_saturne) requires valid `DISPLAY` and X11 access. This is often unavailable on headless HPC nodes. ## Runtime feature differences @@ -33,17 +42,16 @@ Not all features are available across all runtimes: | Feature | native | singularity | docker | |---|:---:|:---:|:---:| -| Slurm (`sbatch`) | ✓ | ✓ | ✗ | -| `mpi_exec_options` | ✓ (Slurm only) | ✓ (Slurm only) | ✗ | +| Slurm (`sbatch`) | ✓ | ✓ | ✓ (needs Docker on the compute nodes) | +| `mpi_exec_options` | ✓ (Slurm only) | ✓ (Slurm only) | ✓ (Slurm only) | | `open_gui` | ✓ | ✓ | ✓ | | Container kill (`docker stop`) | ✗ | ✗ | ✓ | | Container ID tracking (`.csauto.cid`) | ✗ | ✗ | ✓ | -**Docker is incompatible with Slurm.** Even with `use_slurm = true` in `csauto.toml`, -Slurm submission is silently disabled for the Docker runtime. The reason is that Docker -requires a root-owned daemon (`dockerd`) running on every compute node, which is not -available on HPC clusters. If you have a Docker image, convert it to a Singularity image -first: +**Docker rarely works with Slurm.** csauto submits Docker runs to Slurm like the +others, but each compute node then needs a running Docker daemon (`dockerd`), +which HPC clusters seldom provide. If you have a Docker image, convert it to a +Singularity image first: ```bash apptainer pull code_saturne.sif docker://simvia/code_saturne @@ -53,14 +61,16 @@ Then use `runtime = "singularity"` with `use_slurm = true`. ## Scheduler scope -Built-in scheduler integration targets Slurm (`sbatch`/`squeue`/`scancel`), with -opportunistic support for PBS/Torque (`qsub`/`qstat`/`qdel`) and LSF (`bsub`/`bjobs`/`bkill`) -for job kill operations. Without a supported scheduler, launches run as local background processes. +Launching and status use Slurm (`sbatch`, `squeue`). Kill tries `scancel`, `qdel` +(PBS/Torque) and `bkill` (LSF), whichever is installed, on a job id taken from +the registry, `.csauto.jobid` or the case's logs. Without Slurm, launches run as +local background processes. ## Restart dependency -Restart requires valid checkpoint data under `RESU//checkpoint`. -Without checkpoints, restart cannot run. +Restart (code_saturne) requires valid checkpoint data under `RESU//checkpoint`. +Without checkpoints, restart cannot run: the case keeps its status and the +other selected cases still launch. ## Public API exposure diff --git a/docs/quickstart.md b/docs/quickstart.md index fe94132..d542eea 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -3,9 +3,13 @@ This guide walks you from zero to a running campaign with monitoring UI. It covers both **local** (workstation/server) and **Slurm** (HPC cluster) setups. -If you haven't read [Core concepts](./concepts.md) yet, do it first — it takes 5 +If you haven't read [Core concepts](./concepts.md) yet, do it first: it takes 5 minutes and will make this page much clearer. +This guide uses code_saturne. For code_aster, the steps are the same with +`solver = "code_aster"` in `csauto.toml`; [examples/codeaster-cube](../examples/codeaster-cube/README.md) +is a complete code_aster campaign. + --- ## Prerequisites @@ -15,7 +19,7 @@ minutes and will make this page much clearer. - A working code_saturne runtime (one of): - `native`: `code_saturne` binary installed and in `PATH`, or path set via `saturne_bin` - `singularity`: Apptainer or Singularity + a `.sif` image file - - `docker`: Docker engine running + a code_saturne image + - `docker`: Docker engine running (the image defaults to `simvia/code_saturne`) For Slurm: `sbatch`, `squeue`, `scancel` in `PATH`. @@ -77,7 +81,7 @@ port = 8000 ```toml runtime = "docker" -docker_image = "simvia/code_saturne" +docker_image = "simvia/code_saturne" # optional: the default image for code_saturne use_slurm = false max_parallel = 2 @@ -85,7 +89,9 @@ host = "127.0.0.1" port = 8000 ``` -See [config.md](./config.md) for all available keys and their defaults. +`max_parallel` is the default of `csauto run --max-parallel`; in the web UI, the +Run dialog has its own **Max Parallel** field. See [config.md](./config.md) for +all available keys and their defaults. --- @@ -102,7 +108,7 @@ case0003,1.2,kwSST,0.8 case0004,1.0,kwSST,0.8 ``` -- `case_id` is optional — omit it and csauto will generate `case0001`, `case0002`, ... +- `case_id` is optional: omit it and csauto will generate `case0001`, `case0002`, ... - Column names must exactly match the placeholders you use in the template. See [doe-format.md](./doe-format.md) for the full format specification. @@ -125,7 +131,7 @@ TEMPLATE/ Below are two real examples drawn from a `setup.xml`. -**Simple value substitution** — density is read from the DOE column `density_value`: +**Simple value substitution**: density is read from the DOE column `density_value`: ```xml @@ -133,7 +139,7 @@ Below are two real examples drawn from a `setup.xml`. ``` -**Conditional blocks** — the turbulence model section is included only when the +**Conditional blocks**: the turbulence model section is included only when the DOE column `turbulence_model` matches the condition. Only one block is active per case: @@ -154,7 +160,7 @@ per case: ``` -Note that `{ref_v}` is inside the `kwSST` block — it is only active (and +Note that `{ref_v}` is inside the `kwSST` block: it is only active (and therefore required in the DOE) when `turbulence_model == "kwSST"`. For rows where `turbulence_model` is `Spalart-Allmaras`, the entire block is dropped and the value of `ref_v` is ignored. @@ -162,6 +168,7 @@ and the value of `ref_v` is ignored. You can also place placeholders in: - `TEMPLATE/run.cfg` - `TEMPLATE/SRC/*.cpp` (user subroutines) +- any other text file of the template See [doe-format.md](./doe-format.md) for the full placeholder and IF block syntax. @@ -169,7 +176,7 @@ See [doe-format.md](./doe-format.md) for the full placeholder and IF block synta ## 5. Generate cases -> `prepare` is a terminal-only step — there is no UI equivalent. Run it from +> `prepare` is a terminal-only step: there is no UI equivalent. Run it from > the directory that contains `doe.csv` and `TEMPLATE/`. ```bash @@ -185,8 +192,9 @@ Expected output: ``` RUNS/ ├── registry.json +├── campaign.json ← the solver of this campaign (code_saturne) ├── case0001/ -│ ├── DATA/setup.xml ← {u_inlet} replaced with 12.0, etc. +│ ├── DATA/setup.xml ← {density_value} replaced with 1.2, etc. │ └── doe_row.csv ← the DOE values used for this case ├── case0002/ ├── case0003/ @@ -194,21 +202,22 @@ RUNS/ ``` If anything is wrong (missing placeholder column, empty value, missing `setup.xml`), -csauto reports the error before creating any files. +csauto stops with an error naming the problem. --- ## 6. Validate before launching -> `doctor` and `status` are terminal-only steps. Once they pass, everything -> else happens in the web UI. +> `doctor` is terminal-only (`csauto status` is the terminal view of the Status +> panel). Once it passes, everything else happens in the web UI. ```bash csauto doctor RUNS ``` This checks that: -- the runtime binary/image is accessible +- the runtime is available (for Docker, the `docker` command; the image itself + is not checked) - case directories have the expected structure - write access to `RUNS/` is available @@ -258,16 +267,19 @@ See [web-ui.md](./web-ui.md) for the full guide. In the **Status** panel, all your `PREPARED` cases appear in the table. 1. Select all cases: `Ctrl/Cmd + A` -2. Click **Run Selected** +2. Click **Run** 3. Fill in the popup: - - **n** — MPI ranks per case (e.g. `4`) - - **nt** — OpenMP threads per rank (e.g. `2`) - - **max parallel** — how many cases run simultaneously (e.g. `2`) -4. Click **Submit** + - **n**: MPI ranks per case (e.g. `4`) + - **nt**: OpenMP threads per rank (e.g. `2`) + - **Max Parallel**: how many cases run at the same time (e.g. `2`; left empty, + every selected case starts at once) +4. Click **Run** in the popup -Cases move to `RUNNING` status. The table refreshes automatically. +Cases move to `RUNNING`. The case next in line shows `PENDING` while it waits for +a free slot, and the others keep their status until their turn. The table +refreshes automatically. -**On a Slurm cluster**, the same UI flow applies — each case is submitted via +**On a Slurm cluster**, the same UI flow applies: each case is submitted via `sbatch` under the hood. You can verify with `squeue -u "$USER"` in the terminal. ### Alternative: launch from the command line @@ -294,12 +306,12 @@ csauto run RUNS --n 4 --nt 2 --resume Once cases are running, everything can be done from the browser: -- **Status panel** — monitor iteration count, duration, disk usage per case -- **Residuals Plot** — inspect solver convergence curves -- **Log Tail** — stream the `listing` file live (select case → Log Tail tab) -- **Recent Errors** — scan for errors and warnings after a case fails -- **Restart Selected** — restart from checkpoint without touching any files -- **Kill Selected** — stop a running case immediately +- **Status panel**: monitor iteration count, duration, disk usage per case +- **Residuals Plot**: inspect solver convergence curves +- **Log Tail**: stream the solver's log live (`run_solver.log`; select case → Log Tail tab) +- **Recent Errors**: scan for errors and warnings after a case fails +- **Restart**: restart from checkpoint without touching any files +- **Kill**: stop a running case immediately For terminal-based monitoring: @@ -307,8 +319,8 @@ For terminal-based monitoring: # Check status table csauto status RUNS -# Watch a log in real time -csauto tail RUNS --case case0001 --file listing -n 100 +# Watch the solver's log in real time +csauto tail RUNS --case case0001 # Export residuals as CSV + SVG csauto residuals RUNS --case case0001 --out residuals.csv --plot residuals.svg @@ -319,8 +331,9 @@ csauto residuals RUNS --case case0001 --out residuals.csv --plot residuals.svg ## 10. Cleanup In the **Status** panel, select the cases you want to clean and click -**Clean Selected**. A popup lets you choose which RESU runs to keep or delete, -and whether to truncate heavy logs. +**Clean**. A popup lets you choose which RESU runs to keep or delete. Clean also +cuts the solver's logs over 50 MB down to their last 50 MB and removes +`.csauto.cid`. Running and pending cases are left alone. ### Alternative: command line diff --git a/docs/run-lifecycle.md b/docs/run-lifecycle.md index 5feef88..cbc9270 100644 --- a/docs/run-lifecycle.md +++ b/docs/run-lifecycle.md @@ -1,6 +1,6 @@ # Run Lifecycle -A case moves through four states from creation to completion. +A case moves through five states from creation to completion. --- @@ -10,7 +10,10 @@ A case moves through four states from creation to completion. csauto prepare │ ▼ - PREPARED ──────► [run / Run Selected] + PREPARED ──────► [run / Run] + │ + ▼ + PENDING (next in line for a launch slot) │ ▼ RUNNING @@ -22,10 +25,10 @@ csauto prepare │ │ └───────────┬───────────┘ │ - [Restart Selected] + [run / Run / Restart] │ ▼ - RUNNING + PENDING ──► RUNNING ──► ... ``` --- @@ -35,46 +38,70 @@ csauto prepare ### `PREPARED` The case directory has been created by `csauto prepare` and is ready to launch. -No simulation has started yet. +No simulation has started yet. A finished case also returns to `PREPARED` when +the dashboard's Clean deletes all its runs. Relevant files: -- `RUNS/caseXXXX/DATA/setup.xml` — rendered from template -- `RUNS/caseXXXX/doe_row.csv` — DOE values used for this case -- `RUNS/registry.json` — registry entry with `status: PREPARED` +- `RUNS/caseXXXX/DATA/setup.xml`: rendered from template (code_saturne; the + `.export` and `.comm` files for code_aster) +- `RUNS/caseXXXX/doe_row.csv`: DOE values used for this case +- `RUNS/registry.json`: registry entry with `status: PREPARED` +- `RUNS/campaign.json`: the solver of the campaign + +### `PENDING` + +The case is next in line and waits for a free launch slot: fewer running cases +in the campaign than Max Parallel (`--max-parallel` for `csauto run`). The other +cases of the batch keep their status until their turn. If the solver refuses to +launch it (a code_saturne restart without checkpoint, say), the case goes back +to the status it had, and the other cases of the batch still launch. ### `RUNNING` -A simulation is active — either a local background process or a Slurm job. +A simulation is active: either a local background process or a Slurm job. +Every runtime starts the solver inside the case folder. Relevant files: -- `RUNS/caseXXXX/csauto.stdout` — csauto launch output -- `RUNS/caseXXXX/csauto.stderr` — csauto launch errors -- `RUNS/caseXXXX/.csauto.cid` — Docker container ID (Docker runtime only) -- `RUNS/caseXXXX/.csauto.jobid` — Slurm job ID (only when Slurm is active) -- `RUNS/caseXXXX/RESU//listing` — main solver log -- `RUNS/caseXXXX/RESU//run_solver.log` — solver launch log - -Status is refreshed by reading process/job state and log signatures: -- Local (native/singularity): checks if the PID stored in `registry.json` is still alive -- Docker: checks the container ID stored in `.csauto.cid` -- Slurm: calls `squeue` to check if the job ID is still active +- `RUNS/caseXXXX/csauto.stdout`: output of the run command +- `RUNS/caseXXXX/csauto.stderr`: errors of the run command +- `RUNS/caseXXXX/.csauto.cid`: Docker container ID (Docker runtime only) +- `RUNS/caseXXXX/.csauto.jobid`: Slurm job ID (only when Slurm is active) +- `RUNS/caseXXXX/.csauto.exitcode`: exit status of the run command, written + when it ends (local runs and `sbatch --wrap` jobs) +- `RUNS/caseXXXX/RESU//run_solver.log`: code_saturne's main log + (`listing` on older versions) + +Status is refreshed by checking whether the run is still alive: +- Local runs (native, docker, singularity): the PID stored in `registry.json` + (a PID that another process took over, after a reboot for instance, does not count) +- Slurm: `squeue` reports the job ID as active + +A case stays `RUNNING` until its process or job is gone, even after its log +prints a verdict: the solver may still be writing results. Only when `squeue` +cannot tell does a verdict (or a recorded exit status) end the run early. ### `DONE` -The simulation completed successfully. Code_Saturne wrote a normal completion -signature to the log. +The run ended and the solver reported success: code_saturne wrote a normal +completion message to its log, code_aster printed `DIAGNOSTIC JOB : OK` (or +`_ALARM`) in `csauto.stdout`. When the logs give no verdict, an exit status +of 0 in `.csauto.exitcode` means `DONE`. + +Relevant files (code_saturne, in `RESU//`): +- `residuals.csv`: solver residuals per iteration +- `performance.log`: timing and resource metrics +- `checkpoint/`: restart data (if code_saturne was configured to write checkpoints) +- `monitoring/*.csv`: probe output files (if configured) +- `profiles/*.csv`: profile output files (if configured) -Relevant files (in `RESU//`): -- `residuals.csv` — solver residuals per iteration -- `performance.log` — timing and resource metrics -- `checkpoint/` — restart data (if Code_Saturne was configured to write checkpoints) -- `monitoring/*.csv` — probe output files (if configured) -- `profiles/*.csv` — profile output files (if configured) +code_aster writes its results straight into `RESU/`. ### `FAILED` -The simulation terminated abnormally: the process exited with an error, the Slurm -job was cancelled or failed, or an error signature was detected in the log. +The run ended abnormally: the solver reported a failure (code_saturne: an error +in its log or a failure marker in its run folder; code_aster: any other +`DIAGNOSTIC JOB` verdict), or, with no verdict, the run command exited with +another status or recorded none (a cancelled Slurm job, say). --- @@ -91,12 +118,11 @@ Set from UI context menu (right-click) or via the API (`POST /api/case_convergen ## RESU directory structure -Each run attempt creates a new timestamped directory: +With code_saturne, each run attempt creates a new timestamped directory: ``` RUNS/case0001/RESU/ ├── 20260308-1413/ ← first run -│ ├── listing │ ├── run_solver.log │ ├── residuals.csv │ ├── performance.log @@ -108,34 +134,58 @@ RUNS/case0001/RESU/ ``` When you restart a case, a new `RESU//` directory is created alongside -the previous one. The Residuals Plot `Start from = 0` option displays data from -all RESU runs for the full history. +the previous one. The Residuals Plot always includes every run of the case; set +**Start from** to **Zero** to see the full history from iteration 0. -While a case is `RUNNING`, `csauto control` (CLI) and the Stop/Extend buttons -(web UI) drop a `control_file` into the active `RESU//` directory. -code_saturne polls this file once per time step, applies the directive, and -deletes it — so it's a transient handshake file, not part of the persisted -RESU output. +code_aster writes everything into `RESU/` itself, so a case holds one run and a +new run replaces it. + +While a code_saturne case is `RUNNING`, `csauto control` (CLI) and the Control +menu (web UI) drop a `control_file` into the active `RESU//` +directory. code_saturne polls this file once per time step, applies the +directive, and deletes it, so it's a transient handshake file, not part of the +persisted RESU output. + +--- + +## Cleaning runs + +Clean (dashboard) and `csauto cleanup --prune-resu` (CLI) delete only the run +folders the solver reports: each `RESU//` for code_saturne, `RESU` +itself for code_aster. Clean also cuts the solver's logs over 50 MB down to +their last 50 MB and removes `.csauto.cid` (`csauto cleanup` does so with +`--max-log-mb` and `--clear-cid`). Neither touches `RUNNING` or `PENDING` +cases. The dashboard's Clean returns a `DONE` or `FAILED` case to `PREPARED` +only when it leaves the case without any run; `csauto cleanup` leaves statuses +as they are. --- ## Slurm specifics - Job ID is stored in `.csauto.jobid` and in `registry.json` +- `sbatch --wrap` jobs request `--ntasks` from `n` and `--cpus-per-task` from + `nt`; code_saturne with the Singularity runtime submits its own batch script, + `.csauto.slurm.sh`, with the same requests - Status remains `RUNNING` while `squeue` reports the job as active -- When the job disappears from `squeue`, csauto inspects the log to determine - if the outcome was `DONE` or `FAILED` -- `Kill Selected` calls the appropriate scheduler command: `scancel` (Slurm), `qdel` (PBS/Torque), or `bkill` (LSF) +- When the job disappears from `squeue`, csauto reads the solver's verdict in + its logs, else the exit status in `.csauto.exitcode`, to decide between + `DONE` and `FAILED` +- `Kill` cancels the job with `scancel` (it also tries `qdel` and `bkill` when they are installed) + +For a Docker run, `Kill` stops the case's container, found from `.csauto.cid` +or by its `csauto.case_id` and `csauto.campaign` labels, so containers of other +campaigns are never touched. --- ## Per-case history -Every status change and action is appended to: +Every launch, refused launch, kill, control action and dashboard action (notes, +convergence labels, cleanups, file edits...) is appended to: ``` RUNS/caseXXXX/.csauto.history.jsonl ``` Each line is a JSON object with `ts`, `action`, and relevant metadata. -This file is readable with `GET /api/history?case=case0001`. diff --git a/docs/task-cookbook.md b/docs/task-cookbook.md index f41aeea..18a11f2 100644 --- a/docs/task-cookbook.md +++ b/docs/task-cookbook.md @@ -1,7 +1,7 @@ # Task Cookbook Copy/paste recipes for common operations. -The web UI is the primary interface — CLI alternatives are noted where available. +The web UI is the primary interface; CLI alternatives are noted where available. --- @@ -15,15 +15,13 @@ The web UI is the primary interface — CLI alternatives are noted where availab csauto prepare doe.csv TEMPLATE RUNS ``` -Run this again whenever you change `doe.csv` (new rows, new columns) or modify -template placeholders. +Run this again after adding rows to `doe.csv`: unchanged cases are kept and +new ones are added. -Important: - -- `prepare` is incremental for unchanged existing cases: re-running it adds only the - missing cases. -- If an existing case no longer matches the current DOE row or template content, - `prepare` fails for that case. +Important: after adding a column or changing the template, existing cases no +longer match. `prepare` stops at the first one (`Existing case differs from +current DOE/template content`). Generate into a new folder, or remove those +cases first. ### Add new cases to an existing campaign @@ -50,8 +48,8 @@ Fix all `[FAIL]` lines from `doctor` before proceeding. ### Launch all cases -In the **Status** panel: `Ctrl/Cmd + A` → **Run Selected** → fill in `n`, `nt`, -`max parallel` → **Submit**. +In the **Status** panel: `Ctrl/Cmd + A` → **Run** → fill in `n`, `nt`, +`max parallel` → **Run**. CLI alternative: @@ -61,7 +59,7 @@ csauto run RUNS --n 8 --nt 2 --max-parallel 4 ### Launch specific cases only -In the **Status** panel: select the target rows → **Run Selected**. +In the **Status** panel: select the target rows → **Run**. CLI alternative: @@ -71,7 +69,7 @@ csauto run RUNS --n 8 --nt 2 --case case0003 --case case0010 ### Re-run only failed cases -In the **Status** panel: filter by `FAILED` status → select all → **Run Selected**. +In the **Status** panel: filter by `FAILED` status → select all → **Run**. CLI alternative: @@ -81,7 +79,7 @@ csauto run RUNS --n 8 --nt 2 --resume ### Launch on Slurm with many cases -Same UI flow — with `use_slurm = true` in `csauto.toml`, each submission goes +Same UI flow: with `use_slurm = true` in `csauto.toml`, each submission goes through `sbatch` automatically. Verify with `squeue -u "$USER"` in the terminal. CLI alternative: @@ -131,23 +129,25 @@ csauto status RUNS ### Stream a case log in real time -In the **Log Tail** tab: select the case and file (`listing` for the solver log), -set line count, enable auto-scroll. +In the **Log Tail** tab: select the case and file (the solver's main log comes +first: `run_solver.log` for code_saturne, `csauto.stdout` for code_aster), set +line count, enable auto-scroll. CLI alternative: ```bash -# Main solver log -csauto tail RUNS --case case0001 --file listing -n 200 +# Main solver log (the default file) +csauto tail RUNS --case case0001 # Launch log (if process didn't start) -csauto tail RUNS --case case0001 --file csauto.stdout -n 50 --no-follow +csauto tail RUNS --case case0001 --file csauto.stdout --no-follow ``` ### Inspect residuals -In the **Residuals Plot** tab: select case(s), choose physical quantities, -set start point, click **Plot**. +In the **Residuals Plot** tab: select the **Cases** and **Variables**, and +choose **Start from**. The plot updates by itself (**Refresh** when auto-refresh +is off); **Download as PNG** saves it. CLI alternative (export to CSV + SVG): @@ -162,7 +162,8 @@ csauto residuals RUNS --case case0001 --case case0002 \ ### Inspect performance metrics -In the **Timing Snapshot** tab: select a case, click **Load**. +In the **Timing Snapshot** tab: every case with a timing log is listed. Narrow +the list with **Cases**, export it with **Download as CSV**. CLI alternative: @@ -173,8 +174,9 @@ csauto perf RUNS --case case0001 --case case0005 --out perf.csv ### Scan for errors after a failure -In the **Recent Errors** tab: select the case, choose severity (`error`, `warn`, -`all`), click **Scan**. +In the **Recent Errors** tab: select the case in **Cases** and a **Severity** +(All, Error, Warn, Info). The list updates by itself (**Refresh** when +auto-refresh is off). --- @@ -182,16 +184,20 @@ In the **Recent Errors** tab: select the case, choose severity (`error`, `warn`, ### Restart from checkpoint -In the **Status** panel: +Restart is available for code_saturne. In the **Status** panel: 1. Select the case(s) to restart -2. Click **Restart Selected** -3. Choose stop criterion: - - `Iterations` + number of additional iterations - - `Physical time` + additional physical time -4. Click **Submit** - -csauto resolves the latest checkpoint automatically. +2. Click **Restart** +3. Choose the **Mode**, how far to go: + - **Additional iterations**, then the number of iterations + - **Additional physical time**, then the time in seconds +4. With one case selected, **Restart from** picks the run to restart from + (default: **Latest run**, the newest run with a checkpoint). With several + cases, each one restarts from its own latest run. +5. Click **Restart** in the popup + +Through the API, `restart_path` picks the run the same way (for example +`"restart_path": "20260308-1413"`). API alternative: @@ -212,17 +218,19 @@ curl -s -X POST "http://127.0.0.1:8000/api/run_case" \ ## Steer a running case without killing it -A case about to hit its time step limit doesn't need a kill + restart round -trip. Use `csauto control` (or the Status panel's **Stop** / **More ▾** menu) -instead: +A code_saturne case about to hit its time step limit doesn't need a kill + +restart round trip. Use `csauto control` (or the Status panel's **Control** +menu) instead: ```bash -csauto control RUNS case0007 --stop # finish current step, checkpoint, exit -csauto control RUNS case0007 --extend 500 # keep going 500 more time steps -csauto control RUNS case0007 --checkpoint # checkpoint now, keep running -csauto control RUNS case0007 --flush # flush logs/time plots now +csauto control RUNS case0007 stop # finish current step, checkpoint, exit +csauto control RUNS case0007 extend 500 # keep going 500 more time steps +csauto control RUNS case0007 checkpoint # checkpoint now, keep running +csauto control RUNS case0007 flush # flush logs/time plots now ``` +The actions come from the solver; `csauto doctor RUNS` lists them. + API alternative: ```bash @@ -237,9 +245,9 @@ See [docs/cli.md](./cli.md#control) for details on each action. ## Kill running cases -In the **Status** panel: select the running cases → **Kill Selected**. Prefer -**Stop** (above) when you just want the run to wind down cleanly — Kill -discards in-flight work and requires restarting from the last checkpoint. +In the **Status** panel: select the running cases → **Kill**. Prefer the +`stop` control action (above) when you just want the run to wind down cleanly: +Kill discards in-flight work and requires restarting from the last checkpoint. API alternative: @@ -253,8 +261,12 @@ curl -s -X POST "http://127.0.0.1:8000/api/kill_case" \ ## Cleanup -In the **Status** panel: select cases → **Clean Selected**. The popup lets you -choose which RESU runs to keep or delete and whether to truncate heavy logs. +In the **Status** panel: select cases → **Clean**. The popup lets you choose +which runs to keep or delete. Of the results, Clean deletes only the solver's +run folders (each `RESU/` for code_saturne, `RESU` itself for code_aster). +It also cuts the solver's logs over 50 MB down to their last 50 MB and removes +`.csauto.cid`. It skips running and pending cases, and returns a finished case +to `PREPARED` once all its runs are gone. CLI alternative (always preview with `--dry-run` first): @@ -263,7 +275,7 @@ csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 --dry-run csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 --clear-cid ``` -Remove all RESU history (disk space emergency): +Remove every run (disk space emergency): ```bash csauto cleanup RUNS --prune-resu --keep-last 0 @@ -273,13 +285,15 @@ csauto cleanup RUNS --prune-resu --keep-last 0 ## Comparing cases -In the **Multi-Run Compare** tab: +In the **Compare** panel: + +1. Pick the **First case** and the **Second case** (the arrows button swaps them) +2. Pick the **File** (code_saturne: `setup.xml`, `doe_row.csv`, `run_solver.log`, + `performance.log`; code_aster: the export file, `doe_row.csv`) -1. Select cases to compare -2. Choose a base case (reference) -3. Choose file kind (`setup.xml`, `doe_row.csv`, `listing`, ...) -4. Optionally add a regex filter -5. Click **Compare** +The DOE values that differ and the side-by-side diff appear at once; type in +the **Search** box to jump between matching lines. A regex filter is available +through `GET /api/compare_runs` (`filter`). --- @@ -288,7 +302,7 @@ In the **Multi-Run Compare** tab: Right-click a `DONE` or `FAILED` row in the **Status** panel: - **Mark Converged** - **Mark Not Converged** -- **Clear** (remove label) +- **Clear Mark** (remove label) --- diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 085993d..b8fcc58 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -3,7 +3,7 @@ This page maps error messages to their causes and fixes, and covers common diagnostic situations. -**First reflex**: run `csauto doctor RUNS` — it checks your runtime, +**First reflex**: run `csauto doctor RUNS`: it checks your runtime, directory structure, and environment in one command. --- @@ -13,12 +13,16 @@ directory structure, and environment in one command. ### `Variables without matching DOE columns: ...` **Cause**: a placeholder or IF condition variable in the template has no -corresponding column in `doe.csv`. +corresponding column in `doe.csv`. The message names the file that uses it. **Fix**: 1. Add the missing column(s) to the `doe.csv` header 2. Re-run `csauto prepare` +If the braces belong to the file's own language (a Python f-string or set in a +code_aster `.comm` file, C/C++ code), write `\{name}` to keep a literal `{name}` +instead (see [doe-format.md](./doe-format.md#placeholder-syntax)). + --- ### `Missing placeholder values for caseXXXX: ...` @@ -40,18 +44,72 @@ for that case. --- +### `.export file not found in ...` / `Several .export files in ...: keep only one.` + +**Cause**: a code_aster template needs exactly one `.export` file at its root. + +**Fix**: keep a single `*.export` file directly in `TEMPLATE/`. + +--- + +### `RUNS holds a code_saturne campaign; csauto.toml selects code_aster.` + +**Cause**: `csauto prepare` was asked to add cases for one solver to a folder +prepared for another (recorded in `RUNS/campaign.json`). + +**Fix**: prepare into a new folder, or set `solver` in `csauto.toml` to the +campaign's solver. + +--- + +### `Warning: RUNS was prepared for code_saturne: ignoring solver, docker_image, saturne_bin and singularity_image from ...` + +**Cause**: the `csauto.toml` found for this command selects another solver than +the one the campaign was prepared for. csauto uses the campaign's solver, with +that solver's default image and executable: the image and paths in that file +belong to the other solver. + +**Fix**: nothing is needed. To silence it, run the command from the campaign's +own directory or fix `solver` in that `csauto.toml`. To use your own image for +this campaign, pass `--docker-image` (or `--saturne-bin`, `--singularity-image`) +on the command line, or set it in a `csauto.toml` that selects the campaign's +solver. + +--- + +### `RUNS/campaign.json names an unknown solver: 'xyz'` + +**Cause**: the campaign was prepared with a solver this csauto installation +does not have (a newer csauto, say), or `campaign.json` was edited. + +**Fix**: use the csauto that prepared the campaign, or correct the solver name +in `RUNS/campaign.json` (`code_saturne` or `code_aster`). + +--- + ### `No checkpoint found for caseXXXX` **Cause**: a restart was requested but no RESU run contains a `checkpoint/` -directory. +directory. The case keeps its status; the other selected cases still launch. **Fix**: 1. Check that `RESU//checkpoint/` exists and is non-empty -2. If no checkpoint exists, run a fresh simulation first — code_saturne must write +2. If no checkpoint exists, run a fresh simulation first: code_saturne must write at least one checkpoint before a restart is possible --- +### `Run 20260101-1000 of caseXXXX has no checkpoint to restart from` + +**Cause**: the run chosen in the Restart dialog's **Restart from** (or passed +as `restart_path`) holds no checkpoint, usually because it failed before +writing one. The case keeps its status; the other selected cases still launch. + +**Fix**: pick another run, or leave **Restart from** on its default, the most +recent run that has a checkpoint. + +--- + ### `The requested number of time steps ... nt_max ... has to be greater than nt_prev` **Cause**: the restart target iteration is not strictly greater than the number of @@ -62,15 +120,17 @@ checkpoint iteration. --- -### `Invalid restart_mode (must be iterations or physical_time)` +### `Unknown restart_mode '...' (expected: iterations, physical_time)` -**Cause**: invalid `restart_mode` value. +**Cause**: the restart mode is not one the solver offers. The old names +`iteration`, `iter`, `time` and `tmax` are no longer accepted. -**Fix**: set `restart_mode` to exactly `iterations` or `physical_time`. +**Fix**: use one of the names the message lists (`/api/app_config` lists them +as `restart_modes`). --- -### `restart_value must be an integer > 0 for restart_mode=iterations` +### `iterations needs a whole number (Iterations)` / `iterations needs a positive value (Iterations)` **Cause**: the restart increment is not a valid positive integer. @@ -78,7 +138,7 @@ checkpoint iteration. --- -### `restart_value must be > 0 for restart_mode=physical_time` +### `physical_time needs a positive value (Time (s))` **Cause**: the physical time increment is not a valid positive number. @@ -86,9 +146,29 @@ checkpoint iteration. --- +### `csauto control: error: the following arguments are required: action` / `csauto: error: unrecognized arguments: --extend` + +**Cause**: the action is a word after the case, not a flag: the `--stop`, +`--extend N`, `--checkpoint` and `--flush` flags are gone. + +**Fix**: `csauto control RUNS case0007 stop`, or `csauto control RUNS case0007 extend 500`. + +--- + +### `Invalid control action: '...' (expected one of: stop, extend, checkpoint, flush)` + +**Cause**: `csauto control` was given an action the solver does not offer. + +**Fix**: use one of the listed actions; `csauto doctor RUNS` lists them with +the value each one takes. + +--- + ### `n, nt, and max_parallel must be integers > 0` -**Cause**: one of the run parameters is zero, negative, or not an integer. +**Cause**: `n`, `nt` or `max_parallel` sent to the API (`POST /api/run_case`) +is zero or negative (non-integers are refused with `422`). `csauto run` prints +`nprocs and nt must be > 0` or `max_parallel must be > 0` instead. **Fix**: provide strictly positive integers for all three values. @@ -103,10 +183,10 @@ is configured. --- -### `DISPLAY not set (GUI unavailable)` +### `DISPLAY not set (GUI unavailable)` / `DISPLAY not set on server` -**Cause**: a GUI launch (`Open GUI` from the UI) was requested on a headless server -without an X11 display. +**Cause**: `csauto doctor` warns that no X11 display is set; **Open GUI** +(code_saturne) then fails with `DISPLAY not set on server`. **Fix**: configure X11 forwarding or avoid GUI launch on headless nodes. @@ -117,7 +197,7 @@ without an X11 display. **Cause**: the API token is missing or invalid. **Fix**: -- In the UI: click `Token` and enter the correct token +- In the UI: open Settings (gear icon) and enter the token under **API token** - In API calls: send `X-CSAUTO-TOKEN: ` or `Authorization: Bearer ` --- @@ -128,7 +208,7 @@ without an X11 display. 1. Check `RUNS/caseXXXX/csauto.stdout` for launch errors 2. Check `RUNS/caseXXXX/csauto.stderr` for process errors -3. On Slurm: `squeue -u "$USER"` — verify the job is still queued or running +3. On Slurm: `squeue -u "$USER"`, to verify the job is still queued or running 4. Re-run `csauto status RUNS` to force a registry refresh 5. If the process or job is no longer alive, the status should update to `DONE` or `FAILED` on the next refresh @@ -137,38 +217,44 @@ without an X11 display. ### Case finished but status shows `RUNNING` -csauto detects completion from process/job status combined with log signatures. +A case stays `RUNNING` while its process or Slurm job is alive, even after its +log prints the end of the calculation: the solver may still be writing results. Force a refresh: ```bash csauto status RUNS ``` -If it still doesn't update, check `listing` or `run_solver.log` for a normal -completion message. +If it still doesn't update, check whether the run is still alive (`ps`, +`docker ps`, or `squeue`). Once it is gone, the status follows the solver's +verdict in its logs (`run_solver.log` for code_saturne, `csauto.stdout` for +code_aster), or the exit status in `.csauto.exitcode`. --- -### Runtime not found (docker / code_saturne / apptainer) +### Runtime not found (docker / code_saturne / run_aster / apptainer) -1. Run `csauto doctor RUNS` — it will identify the exact failing check +1. Run `csauto doctor RUNS`: it will identify the exact failing check 2. Verify the binary is in `PATH` or set the explicit path in `csauto.toml`: - - `saturne_bin = "/path/to/code_saturne"` + - `saturne_bin = "/path/to/code_saturne"` (or to `run_aster` for code_aster) - `singularity_bin = "/path/to/apptainer"` - `singularity_image = "/path/to/image.sif"` - - `docker_image = "image:tag"` (and confirm Docker daemon is running) + - `docker_image = "image:tag"` (optional, the solver's own image by default; + confirm the Docker daemon is running) --- ### Missing residuals, probes, or profiles in the UI -- **Residuals**: check `RESU//residuals.csv` — if missing, csauto falls +These panels exist for code_saturne only. + +- **Residuals**: check `RESU//residuals.csv`; if missing, csauto falls back to parsing `run_solver.log` - **Probes**: check `RESU//monitoring/*.csv` - **Profiles**: check `RESU//profiles/*.csv` -If you restarted a case and data from the first run seems missing, use -`Start from = 0` in the Residuals Plot to include all RESU directories. +The Residuals Plot always includes every run of the case: after a restart, set +**Start from** to **Zero** to see the history from iteration 0. --- @@ -192,7 +278,10 @@ Always test with `--dry-run` first: csauto cleanup RUNS --prune-resu --keep-last 1 --max-log-mb 100 --dry-run ``` -This prints what would be deleted without actually deleting anything. +This prints how many run folders and logs would be affected, without deleting +anything. `--prune-resu` only deletes the run folders the solver reports (each +`RESU/` for code_saturne, `RESU` itself for code_aster), and cleanup skips +`RUNNING` and `PENDING` cases. --- @@ -206,4 +295,5 @@ Fix every `[FAIL]` line before launching. Common causes: - Runtime binary not found - Image file not accessible - `RUNS/` directory not writable -- No `case*` directories found in `RUNS/` +- No case found in `RUNS/` (run `csauto prepare` first) +- The solver's setup file missing from some cases diff --git a/docs/web-ui.md b/docs/web-ui.md index 056e16c..a5f9338 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -28,13 +28,20 @@ The header always shows: When a filter is active, each metric shows the filtered count out of the total (e.g., "2 / 5"). -Branding is solver-aware: code_saturne campaigns show the Code_Saturne logo in -the header, other solvers show their name as text, and the browser-tab favicon -switches to a solver-specific icon when the frontend ships one. +The header shows the solver's logo and the browser tab its icon, both provided +by the solver adapter; a solver without a logo shows its name as text. Until +the dashboard has loaded the solver's description from the server, it shows +only the Status, Log Tail and Recent Errors panels and no solver action. + +The panels and buttons depend on the solver: each one appears only when the +solver provides what it needs (see +[Adding a new solver](./adding-a-solver.md#4-fill-the-dashboard)). The examples +below use code_saturne, which uses all of them. **Settings** (gear icon, top right): opens a dialog where you can set the **API token** (if the server requires authentication) and the **auto-refresh -rate**. The token is stored in the browser session. +rate**. The token is saved in the browser (localStorage), so it is asked once per +browser and server address. --- @@ -60,10 +67,10 @@ The toolbar provides: | `THREAD COUNT` | Number of OpenMP threads per rank | | `LAST ITER` | Last completed iteration | | `DURATION` | Elapsed time since launch | -| `RESU SIZE (MB)` | Total RESU disk usage | +| `RESULTS (MB)` | Disk usage of the case's results folder | | `LAST MODIFIED` | Timestamp of the last state change | -Each row also has an **Open GUI** button to launch the code_saturne GUI for that case. +When the solver has a GUI (code_saturne does), each row also has an **Open GUI** button that opens it on the case's setup file. ### Selecting cases @@ -80,46 +87,52 @@ Each row also has an **Open GUI** button to launch the code_saturne GUI for that With one or more cases selected, the action buttons activate: **Run**: launch the selected cases (`PREPARED`, `DONE`, or `FAILED`). A popup -asks for `--n` (MPI ranks), `--nt` (OMP threads), and `--max-parallel`. - -**Restart**: restart from the latest checkpoint. A popup asks for the -stop criterion: -- `Iterations`: add N more iterations beyond the current checkpoint -- `Physical time`: run until a physical time target - -csauto automatically finds the latest checkpoint and computes the absolute -target value. You do not need to know the checkpoint path. - -**Stop**: gracefully stop the selected running cases — code_saturne finishes -its current time step, writes a checkpoint, and exits on its own. Unlike -Kill, no process is signaled and no restart is needed afterwards. - -**More ▾**: secondary controls for running cases, next to Stop: -- **Extend**: raise the time step limit so the case keeps going instead of - stopping. A popup asks how many additional time steps to add to the case's - configured limit (repeated extends stack correctly). -- **Checkpoint**: request a checkpoint at the next time step, without - stopping the run. Useful for grabbing a restart point mid-run. This does - not add a marker to the Residuals/Probes plots — those only mark actual - restarts (a new run launched from a checkpoint), not in-place checkpoints. -- **Flush**: force logs and time-plot/probe files to be written to disk - immediately, without waiting for the next automatic write. Doesn't affect - the simulation itself. - -**Kill**: send a termination signal to the selected running cases. -Works for both local processes and Slurm jobs. Use Stop instead when you -just want the run to wind down cleanly — Kill discards in-flight work and -requires restarting from the last checkpoint. - -**Clean**: remove old RESU directories and/or truncate heavy logs for -the selected cases. A popup lets you choose which RESU runs to keep or delete. +asks for **MPI Ranks (n)**, **OMP Threads (nt)** and **Max Parallel** (left +empty, every selected case starts at once). + +**Restart** (when the solver supports it): continue finished cases. A popup +offers the solver's restart modes and asks for a value when the mode takes +one. With code_saturne: +- `Additional iterations`: add N more iterations beyond the checkpoint +- `Additional physical time`: run for that much more physical time + +**Restart from** picks the run to restart from (one case selected); by default +csauto uses the latest run that has a checkpoint and computes the absolute +target value for you. + +**Control** (when the solver supports live control): a menu of the actions the +solver offers for running cases. Actions that take a value ask for it; the +others ask for confirmation. With code_saturne: +- **Stop gracefully**: finish the current time step, write a checkpoint and + exit. Unlike Kill, no process is signaled and no restart is needed. +- **Extend**: raise the time step limit so the case keeps going. Asks for the + number of additional time steps (repeated extends stack correctly). +- **Write a checkpoint**: at the next time step, without stopping the run. + Plots mark restarts (new runs launched from a checkpoint), not in-place + checkpoints. +- **Flush logs and plots**: write logs and probe files to disk now. It does + not affect the simulation. + +**Kill**: stop the selected running cases at once. Works for local processes, +containers and Slurm jobs. Prefer a graceful stop when the solver offers one: +Kill discards in-flight work. + +**Clean**: delete old run folders and/or shorten heavy logs for the selected +finished cases. A popup lets you keep the latest N runs, delete them all, or, +with a single case selected, pick the run folders to keep or delete (run +folders are named per case). Clean only deletes the folders the solver reports +as runs (code_saturne: each `RESU/`; code_aster: `RESU` itself, its single +run) and never touches running or queued cases; a notice lists the ones it +skipped. A case whose runs were all deleted returns to `PREPARED`. Clean +also shortens logs larger than 50 MB to their last 50 MB and removes the +docker container id file (`.csauto.cid`). ### Context menu on finished cases Right-click a `DONE` or `FAILED` row to: - **Mark Converged**: set convergence label to `converged` - **Mark Not Converged**: set to `not converged` -- **Clear**: remove the convergence label +- **Clear Mark**: remove the convergence label --- @@ -132,7 +145,7 @@ Use this panel to inspect solver convergence for one or more cases. 1. Select one or more cases from the **Cases** dropdown 2. Choose the **Variables** to plot (e.g., velocity, pressure) 3. Choose the **Start from** point: - - `Zero`: plot from the very beginning, including previous runs in RESU history + - `Zero`: plot from the very beginning, including the case's previous runs - `Restart start`: start from the last restart checkpoint iteration - `Custom`: manually set the minimum iteration to display 4. The chart updates automatically when auto-refresh is enabled @@ -148,12 +161,12 @@ Click **Download as PNG** to export the chart. ![Probes and Profiles](./assets/ui-probes.png) -This panel has two tabs — **Probes** and **Profiles** — each shown only when -the relevant output files exist in the case's RESU directory. +This panel has two tabs, **Probes** and **Profiles**, each shown when the case +has such files (with code_saturne, in its latest `RESU/`). ### Probes tab -Shown when `RESU//monitoring/*.csv` files exist. +With code_saturne: `RESU//monitoring/*.csv`. 1. Select one or more cases from the **Cases** dropdown 2. Choose the **Quantity** (probe file to read, e.g., CourantNb) @@ -165,7 +178,7 @@ The spatial coordinates of each selected probe are displayed below the controls. ### Profiles tab -Shown when `RESU//profiles/*.csv` files exist. +With code_saturne: `RESU//profiles/*.csv`. 1. Select one or more cases from the **Cases** dropdown 2. Choose the **Profile** file @@ -212,9 +225,10 @@ Steps: swap button to switch them) 2. The panel shows a **parameter diff** summary (e.g., "1 difference out of 3 parameters"). Click **Show all parameters** to see matching parameters too -3. Choose a **File** to compare — the list is declared by the solver adapter, - and its first entry is the default (with code_saturne: `setup.xml`, - `doe_row.csv`, `run_solver.log`, or `performance.log`) +3. Choose a **File** to compare: the list comes from the solver, and its first + entry is the default (code_saturne: `setup.xml`, `doe_row.csv`, + `run_solver.log`, `performance.log`; code_aster: the export file and + `doe_row.csv`) 4. Use the **Search** bar to filter lines The output shows a side-by-side diff with line numbers. Differing lines are @@ -228,13 +242,15 @@ highlighted for quick identification. Stream the end of a case log file, similar to `tail -f`. -**Which file to read:** +The **File** list puts the solver's main logs first, then `csauto.stdout` and +`csauto.stderr` (the console output of the launch, available for every solver) +and the other `*.log` files of the latest run. The first file is selected by +default. | Situation | File to use | |---|---| -| Solver running, check progress | `listing` | -| Solver error or crash | `run_solver.log` | -| Launch issue (process didn't start) | `csauto.stdout` / `csauto.stderr` | +| Solver running, check progress | the solver's main log (code_saturne: `run_solver.log`; code_aster: `csauto.stdout`) | +| Launch issue (the solver did not start) | `csauto.stderr` / `csauto.stdout` | | Slurm submission issue | `csauto.stdout` | Controls: @@ -242,7 +258,8 @@ Controls: - **File**: choose the log file - **Lines**: number of lines to display (default 80) - **Filter**: regex filter to match specific lines -- **Severity**: filter by `error`, `warn`, or `All` +- **Severity**: filter by `Error`, `Warn` or `All`. Lines are coloured + with the same patterns Recent Errors uses for this solver The bottom bar shows: - A **line count** indicator (filtered lines / total, e.g., "80 / 80") @@ -264,10 +281,10 @@ want a quick summary of what went wrong without reading the full log. Controls: - **Cases**: select which cases to scan -- **Files**: select which log files to include (multi-select; the list is - declared by the solver adapter — with code_saturne: `csauto.stderr`, - `run_solver.log`, `listing`, `csauto.stdout`) -- **Severity**: `All`, `Error`, `Warn`, `Info` +- **Files**: select which log files to include (multi-select; the list comes + from the solver; with code_saturne: `csauto.stderr`, `run_solver.log`, + `listing`, `csauto.stdout`) +- **Severity**: `All`, `Error`, `Warn` - **Search**: plain-text search within matched lines - **Context**: how many lines before and after each hit to display (default 6) @@ -303,4 +320,4 @@ csauto serve RUNS --host 0.0.0.0 --port 8000 Without a token, csauto refuses to bind to a public address. Users accessing the UI must click the **Settings** gear icon and enter the token -once per browser session. +once per browser. diff --git a/examples/codeaster-cube/README.md b/examples/codeaster-cube/README.md index 869e23a..fe04e44 100644 --- a/examples/codeaster-cube/README.md +++ b/examples/codeaster-cube/README.md @@ -6,19 +6,19 @@ A complete campaign showcasing the main csauto features with code_aster. Simulates a cube under triaxial traction. The DOE combines: -- **2 values of imposed displacement along x, y or z** — 1.0e-3 and 0.0 -- **2 meshes** — `mesh1.med` and `mesh2.med` -- **1 additional case** - with non valid values for imposed displacements +- **2 values of imposed displacement along x, y or z**: 1.0e-3 and 0.0 +- **2 meshes**: `mesh1.med` and `mesh2.med` +- **1 additional case** with an invalid imposed displacement, which fails on purpose This produces **9 cases**. The template demonstrates: -- **value placeholders** — `{depl_x}`, `{depl_y}`, `{depl_z}`, `{mesh_name}` in both `export` and `comm` files -- **conditional way in command file** — with an `if` situation related to values of the DOE +- **value placeholders**: `{depl_x}`, `{depl_y}`, `{depl_z}` in the `comm` file and `{mesh_name}` in the `export` file +- **a condition in the command file**: a Python `if` on the DOE values, which stops the invalid case with an error ## Files ``` codeaster-cube/ -├── csauto.toml ← runtime configuration (Docker) +├── csauto.toml ← solver and runtime configuration (Docker) ├── doe.csv ← 9 cases ├── TEMPLATE/ │ ├── study.comm ← a code_aster command file @@ -29,22 +29,29 @@ codeaster-cube/ └── mesh2.med ← second mesh variant ``` -> **WARNING**: As the project currently stands, in an `export` file contained in `TEMPLATE`, the paths specified for the input meshes must be prefixed with the `MESH/` path, and the output files to be saved must be prefixed with the `RESU/` path. +`MESH` is shared by every case: `csauto prepare` places it next to the cases +(`RUNS/MESH`), so `study.export` references a mesh as `../MESH/{mesh_name}`. +Results are written under `RESU/` (`RESU/myresults.rmed`), the folder csauto +treats as the case's run: its size shows in the status table, and Clean deletes +it when asked to keep no run. ## Prerequisites - csauto installed (`csauto --version` works) -- A working code_aster runtime: Docker or Singularity image (native is coming) +- A working code_aster runtime: Docker (the default image is + `simvia/code_aster:17.4.0`), a Singularity image, or `run_aster` installed natively ## 1. Configure your runtime A `csauto.toml` is already provided with a Docker configuration. Edit it to -match your environment, or replace it entirely. Examples: +match your environment, or replace it entirely. Keep `solver = "code_aster"`: +without it, csauto expects a code_saturne case. Examples: **Docker (provided):** ```toml +solver = "code_aster" runtime = "docker" -docker_image = "simvia/code_aster:17.4.0" +docker_image = "simvia/code_aster:17.4.0" # the default for code_aster host = "127.0.0.1" port = 8000 @@ -55,6 +62,7 @@ token = "your-secret-token" **Singularity image:** ```toml +solver = "code_aster" runtime = "singularity" singularity_image = "/path/to/code_aster_17.4.0.sif" @@ -65,6 +73,19 @@ port = 8000 token = "your-secret-token" ``` +**Native `run_aster`:** +```toml +solver = "code_aster" +runtime = "native" +saturne_bin = "/path/to/run_aster" # optional when run_aster is in PATH + +host = "127.0.0.1" +port = 8000 + +[api] +token = "your-secret-token" +``` + ## 2. Generate the case ```bash @@ -76,8 +97,9 @@ Expected output: ``` RUNS/ ├── registry.json +├── campaign.json ← records the code_aster solver +├── MESH/ ← link to the example's MESH folder (mesh_mode = "symlink") ├── case0001/ -│ ├── MESH/ ← selected mesh from MESH/ │ ├── study.comm ← a copy of the code_aster command file with the input values │ ├── study.export ← a copy of the code_aster export file with the input values │ └── doe_row.csv @@ -106,11 +128,17 @@ Open [http://127.0.0.1:8000](http://127.0.0.1:8000). In the **Status** panel: 1. Select one or more cases (`Ctrl+A` to select all) -2. Click **Run Selected** +2. Click **Run** 3. Set **n** = 1 (MPI rank), **nt** = 1 (thread) -4. Click **Submit** +4. Click **Run** in the popup + +csauto writes **n** and **nt** into `.csauto.export` (as `mpi_nbcpu` and +`ncpus`), a copy of the case's `study.export` that `run_aster` runs; the case's +own export is never modified. -Cases move to `RUNNING`. Once complete they show `DONE`. +Cases move to `RUNNING`. Once complete they show `DONE`, except `case0001`, +whose invalid displacement stops the command file: it shows `FAILED`, with +`DIAGNOSTIC JOB : _ABNORMAL_ABORT` in its `csauto.stdout`. ## Alternative: launch from the terminal @@ -121,6 +149,11 @@ csauto run RUNS --n 1 --nt 1 ## Explore the results From the web UI: -- **Residuals Plot** — not in the current version `#TODO` -- **Probes** — not in the current version `#TODO` -- **Log Tail** — live solver output +- **Log Tail**: `csauto.stdout` (the output of `run_aster`, which ends with the + `DIAGNOSTIC JOB` verdict), the `output.mess` message file, and `csauto.stderr` +- **Recent Errors**: code_aster alarms (``) and errors (``, ``, ``) + from `csauto.stdout` and `csauto.stderr` +- **Compare**: the export file or `doe_row.csv`, side by side across cases + +Residuals, probes and timings are not available for code_aster. The results +themselves are in `RUNS/caseXXXX/RESU/myresults.rmed`. diff --git a/examples/codeaster-cube/TEMPLATE/study.export b/examples/codeaster-cube/TEMPLATE/study.export index d4ac61b..75af6af 100644 --- a/examples/codeaster-cube/TEMPLATE/study.export +++ b/examples/codeaster-cube/TEMPLATE/study.export @@ -11,7 +11,7 @@ P mpi_nbnoeud 1 # Input files F comm study.comm D 1 -F mmed MESH/{mesh_name} D 20 +F mmed ../MESH/{mesh_name} D 20 # Output files F rmed RESU/myresults.rmed R 80 diff --git a/examples/single-case/README.md b/examples/single-case/README.md index 182b5f6..2df01bd 100644 --- a/examples/single-case/README.md +++ b/examples/single-case/README.md @@ -9,16 +9,17 @@ that your csauto installation and code_saturne runtime are working end to end. Simulates an internal flow over a stepped channel geometry with an immersed cylinder (volume-penalization IBM). The DOE combines: -- **2 fluid densities** — 1.1 and 1.2 kg/m³ -- **2 turbulence models** — Spalart-Allmaras and k-omega SST -- **2 meshes** — `mesh1.med` and `mesh2.med` -- **2 penalization coefficients** — K0 = 1000 and 2000 - -This produces **8 cases** (full factorial). The template demonstrates: -- **value placeholders** — `{density_value}`, `{ref_v}`, `{mesh}`, `{K0}` -- **conditional blocks** — `` to switch +- **2 fluid densities**: 1.1 and 1.2 kg/m³ +- **2 turbulence models**: Spalart-Allmaras and k-omega SST +- **2 meshes**: `mesh1.med` and `mesh2.med` +- **2 penalization coefficients**: K0 = 1000 (with `mesh1.med`) and 2000 (with `mesh2.med`) + +This produces **8 cases**: 2 densities × 2 models × 2 meshes, each mesh with its +own K0. The template demonstrates: +- **value placeholders**: `{density_value}`, `{ref_v}`, `{mesh}`, `{K0}` +- **conditional blocks**: `` to switch between Spalart-Allmaras and k-omega SST sections -- **user source compilation** — `TEMPLATE/SRC/cs_user_source_terms.cpp` with a +- **user source compilation**: `TEMPLATE/SRC/cs_user_source_terms.cpp` with a `{K0}` placeholder, compiled by code_saturne at run time ## Files @@ -96,10 +97,12 @@ Expected output: ``` RUNS/ ├── registry.json +├── campaign.json ← records the code_saturne solver +├── MESH/ ← link to single-case/MESH (mesh_mode = "symlink") +├── POST/ ← link to single-case/POST ├── case0001/ │ ├── DATA/setup.xml ← all placeholders substituted, IF blocks resolved │ ├── SRC/ ← cs_user_source_terms.cpp with K0 substituted -│ ├── MESH/ ← selected mesh copied from single-case/MESH/ │ └── doe_row.csv ├── case0002/ │ └── ... @@ -126,9 +129,9 @@ Open [http://127.0.0.1:8000](http://127.0.0.1:8000). In the **Status** panel: 1. Select one or more cases (`Ctrl+A` to select all) -2. Click **Run Selected** +2. Click **Run** 3. Set **n** = 1 (MPI rank), **nt** = 1 (thread) -4. Click **Submit** +4. Click **Run** in the popup Cases move to `RUNNING`. Once complete they show `DONE`. @@ -141,6 +144,6 @@ csauto run RUNS --n 1 --nt 1 ## Explore the results From the web UI: -- **Residuals Plot** — convergence curves -- **Log Tail** — live solver output (`listing` file) -- **Probes** — time series at the two probe points defined in the mesh +- **Residuals Plot**: convergence curves +- **Log Tail**: live solver output (`run_solver.log`) +- **Probes**: time series at the two probe points defined in `setup.xml` diff --git a/frontend/dist/_app/immutable/assets/2.DKE6ochW.css b/frontend/dist/_app/immutable/assets/2.Bl1bTUX7.css similarity index 83% rename from frontend/dist/_app/immutable/assets/2.DKE6ochW.css rename to frontend/dist/_app/immutable/assets/2.Bl1bTUX7.css index 3454f86..2857cb9 100644 --- a/frontend/dist/_app/immutable/assets/2.DKE6ochW.css +++ b/frontend/dist/_app/immutable/assets/2.Bl1bTUX7.css @@ -1 +1 @@ -/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:"JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New", monospace;--color-red-600:oklch(57.7% .245 27.325);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-light:300;--font-weight-normal:400;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"edf-2020";--default-mono-font-family:var(--font-mono);--color-edf-orange-fonce:#d6430a;--color-edf-orange-moyen:#ff861d;--color-edf-orange-clair:#ffb210;--color-edf-vert-fonce:#307a10;--color-edf-bleu-fonce:#001a70;--color-edf-bleu-moyen:#1057c8;--color-edf-bleu-clair:#1089ff;--color-edf-noir:#333;--color-edf-slate:#f8f7f7;--color-edf-gris-moyen:#dedede;--color-edf-gris-clair:#f8f7f7;--color-edf-blanc:#fff;--color-bg:var(--color-edf-slate);--color-ink:var(--color-edf-noir);--color-muted:#787878;--color-card:var(--color-edf-blanc);--color-border:var(--color-edf-gris-moyen);--color-table-head:var(--color-edf-gris-clair);--color-table-row:var(--color-edf-blanc);--color-table-hover:#f5f5f4;--color-table-selected:#e8eff8;--color-table-selected-hover:#dce7f4;--animate-rise:rise .35s ease both}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.sticky\!{position:sticky!important}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.right-0{right:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-80{z-index:80}.z-\[1\]{z-index:1}.z-\[1000\]{z-index:1000}.col-span-6{grid-column:span 6/span 6}.col-span-12{grid-column:span 12/span 12}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing) * 0)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-auto{margin-right:auto}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-4\.5{margin-bottom:calc(var(--spacing) * 4.5)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[900\/500\]{aspect-ratio:900/500}.h-7{height:calc(var(--spacing) * 7)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-\[3px\]{height:3px}.h-\[10px\]{height:10px}.h-\[14px\]{height:14px}.h-\[30px\]{height:30px}.h-\[34px\]{height:34px}.h-full{height:100%}.max-h-\[240px\]{max-height:240px}.max-h-\[260px\]{max-height:260px}.max-h-\[500px\]{max-height:500px}.\!min-h-\[80px\]{min-height:80px!important}.w-9{width:calc(var(--spacing) * 9)}.w-32\.5{width:calc(var(--spacing) * 32.5)}.w-37\.5{width:calc(var(--spacing) * 37.5)}.w-\[10px\]{width:10px}.w-\[14px\]{width:14px}.w-\[30px\]{width:30px}.w-\[40px\]{width:40px}.w-\[70px\]{width:70px}.w-\[72px\]{width:72px}.w-\[80px\]{width:80px}.w-\[90px\]{width:90px}.w-\[110px\]{width:110px}.w-\[130px\]{width:130px}.w-\[140px\]{width:140px}.w-\[150px\]{width:150px}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[200px\]{width:200px}.w-\[min\(420px\,96vw\)\]{width:min(420px,96vw)}.w-\[min\(1200px\,94vw\)\]{width:min(1200px,94vw)}.w-\[min\(\{maxWidth\}\,96vw\)\]{width:min({maxWidth},96vw)}.w-auto{width:auto}.w-full{width:100%}.w-max{width:max-content}.max-w-\[140px\]{max-width:140px}.max-w-\[320px\]{max-width:320px}.max-w-\[920px\]{max-width:920px}.\!min-w-0{min-width:calc(var(--spacing) * 0)!important}.min-w-\[36px\]{min-width:36px}.min-w-\[40px\]{min-width:40px}.min-w-\[72px\]{min-width:72px}.min-w-\[170px\]{min-width:170px}.min-w-\[200px\]{min-width:200px}.min-w-full{min-width:100%}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.animate-pulse{animation:var(--animate-pulse)}.animate-rise{animation:var(--animate-rise)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.self-end{align-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-\[rgba\(51\,51\,51\,0\.08\)\]{border-color:#33333314}.border-border{border-color:var(--color-border)}.border-edf-bleu-moyen{border-color:var(--color-edf-bleu-moyen)}.border-edf-gris-moyen{border-color:var(--color-edf-gris-moyen)}.border-transparent{border-color:#0000}.bg-\[rgba\(0\,26\,112\,0\.3\)\]{background-color:#001a704d}.bg-\[rgba\(16\,87\,200\,0\.06\)\]{background-color:#1057c80f}.bg-\[rgba\(48\,122\,16\,0\.04\)\]{background-color:#307a100a}.bg-\[rgba\(48\,122\,16\,0\.06\)\]{background-color:#307a100f}.bg-\[rgba\(48\,122\,16\,0\.08\)\]{background-color:#307a1014}.bg-\[rgba\(214\,67\,10\,0\.04\)\]{background-color:#d6430a0a}.bg-\[rgba\(214\,67\,10\,0\.06\)\]{background-color:#d6430a0f}.bg-\[rgba\(214\,67\,10\,0\.08\)\]{background-color:#d6430a14}.bg-\[rgba\(214\,67\,10\,0\.12\)\]{background-color:#d6430a1f}.bg-\[rgba\(255\,178\,16\,0\.1\)\]{background-color:#ffb2101a}.bg-\[rgba\(255\,178\,16\,0\.3\)\]{background-color:#ffb2104d}.bg-\[rgba\(255\,178\,16\,0\.6\)\]{background-color:#ffb21099}.bg-card{background-color:var(--color-card)}.bg-edf-blanc{background-color:var(--color-edf-blanc)}.bg-edf-bleu-moyen{background-color:var(--color-edf-bleu-moyen)}.bg-edf-gris-clair{background-color:var(--color-edf-gris-clair)}.bg-edf-orange-fonce{background-color:var(--color-edf-orange-fonce)}.bg-edf-orange-moyen{background-color:var(--color-edf-orange-moyen)}.bg-edf-vert-fonce{background-color:var(--color-edf-vert-fonce)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-4\.5{padding:calc(var(--spacing) * 4.5)}.p-5{padding:calc(var(--spacing) * 5)}.p-\[16px_18px_18px\]{padding:16px 18px 18px}.\!px-2\.5{padding-inline:calc(var(--spacing) * 2.5)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-\[1px\]{padding-inline:1px}.px-\[10px\]{padding-inline:10px}.\!py-0{padding-block:calc(var(--spacing) * 0)!important}.\!py-1{padding-block:calc(var(--spacing) * 1)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-\[7px\]{padding-block:7px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-7{padding-right:calc(var(--spacing) * 7)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[edf-2020-soft\]{font-family:edf-2020-soft}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[1\.45\]{--tw-leading:1.45;line-height:1.45}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-edf-bleu-clair{color:var(--color-edf-bleu-clair)}.text-edf-bleu-fonce{color:var(--color-edf-bleu-fonce)}.text-edf-bleu-moyen{color:var(--color-edf-bleu-moyen)}.text-edf-gris-moyen{color:var(--color-edf-gris-moyen)}.text-edf-noir{color:var(--color-edf-noir)}.text-edf-orange-fonce{color:var(--color-edf-orange-fonce)}.text-edf-vert-fonce{color:var(--color-edf-vert-fonce)}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.italic{font-style:italic}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-\[background\]{transition-property:background;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-\[120ms\]{--tw-duration:.12s;transition-duration:.12s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.hover\:border-border:hover{border-color:var(--color-border)}.hover\:border-edf-bleu-clair:hover{border-color:var(--color-edf-bleu-clair)}.hover\:border-edf-gris-moyen:hover{border-color:var(--color-edf-gris-moyen)}.hover\:bg-\[rgb\(12\,72\,170\)\]:hover{background-color:#0c48aa}.hover\:bg-\[rgb\(38\,98\,12\)\]:hover{background-color:#26620c}.hover\:bg-\[rgb\(180\,56\,8\)\]:hover{background-color:#b43808}.hover\:bg-\[rgb\(230\,118\,20\)\]:hover{background-color:#e67614}.hover\:bg-edf-gris-clair:hover{background-color:var(--color-edf-gris-clair)}.hover\:text-muted:hover{color:var(--color-muted)}.hover\:underline:hover{text-decoration-line:underline}.hover\:decoration-dotted:hover{text-decoration-style:dotted}.hover\:underline-offset-2:hover{text-underline-offset:2px}}.focus\:border-edf-bleu-moyen:focus{border-color:var(--color-edf-bleu-moyen)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}@media not all and (min-width:64rem){.max-lg\:order-3{order:3}.max-lg\:h-auto{height:auto}.max-lg\:w-full{width:100%}.max-lg\:flex-wrap{flex-wrap:wrap}.max-lg\:gap-2{gap:calc(var(--spacing) * 2)}.max-lg\:gap-3{gap:calc(var(--spacing) * 3)}}}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Regular.DEtAzL8F.woff2)format("woff2");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Bold.Bub472Sg.woff2)format("woff2");font-weight:700;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Light.D7COPh2m.woff2)format("woff2");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Italic.CtMn0wF8.woff2)format("woff2");font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-BoldItalic.y4KHcb-c.woff2)format("woff2");font-weight:700;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-Italic.CTdZcEjD.woff2)format("woff2");font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-LightItalic.DlUXaATs.woff2)format("woff2");font-weight:300;font-style:italic;font-display:swap}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{color:var(--color-ink);background:var(--color-bg);min-height:100vh;margin:0}input,select,textarea{border:1px solid var(--color-border);height:30px;color:var(--color-ink);background:#fff;border-radius:6px;padding:5px 10px;font-family:inherit;font-size:13px;transition:border-color .15s}textarea,select[multiple]{height:auto}input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}input:focus,select:focus,textarea:focus{border-color:var(--color-edf-bleu-moyen);outline:none}select{min-width:160px}select[multiple]{min-width:180px}textarea{width:100%;min-height:160px;font-family:var(--font-mono);font-size:12px}[data-icon]:before{content:attr(data-icon);opacity:.8;justify-content:center;align-items:center;font-size:12px;display:inline-flex}.table-wrap{border:1px solid var(--color-border);background:var(--color-table-row);border-radius:8px;overflow-x:auto}#status-table{border-collapse:separate;border-spacing:0;width:max-content;min-width:100%}table{border-collapse:collapse;width:100%;min-width:760px}th,td{text-align:left;border-bottom:1px solid var(--color-edf-gris-clair);padding:8px 10px;font-size:13px}th{text-transform:uppercase;letter-spacing:.12em;color:var(--color-muted);background:var(--color-table-head);z-index:1;font-size:11px;font-weight:700;position:sticky;top:0}th.status-col-doe,th.status-col-calc{white-space:nowrap}td.status-col-doe{font-family:var(--font-mono);font-size:12px}td.case-id{font-weight:700}tbody tr{background:var(--color-table-row)}tbody tr:hover{background:var(--color-table-hover)}tbody tr.row-selected{background:var(--color-table-selected)}tbody tr.row-selected:hover{background:var(--color-table-selected-hover)}#status-body tr,#status-body td{-webkit-user-select:none;user-select:none}th.status-sortable{cursor:pointer;-webkit-user-select:none;user-select:none}th.status-sortable:hover{color:var(--color-edf-bleu-moyen)}#status-doe-select{min-width:220px;max-width:360px}#status-table thead tr,#status-table tbody tr{height:34px}#status-table-wrap{max-height:374px;overflow-y:auto}#status-table-wrap.sticky-enabled th.sticky-left,#status-table-wrap.sticky-enabled th.sticky-right{background:var(--color-table-head);z-index:4;position:sticky}#status-table-wrap.sticky-enabled td.sticky-left,#status-table-wrap.sticky-enabled td.sticky-right{background:var(--color-table-row);z-index:3;position:sticky}#status-table-wrap.sticky-enabled .sticky-right{right:0}#status-table-wrap.sticky-enabled .sticky-divider{border-right:1px solid var(--color-border)}#status-table-wrap.sticky-enabled .sticky-right.sticky-divider{border-left:1px solid var(--color-border);border-right:none}#status-table-wrap.sticky-enabled tbody tr:hover td.sticky-left,#status-table-wrap.sticky-enabled tbody tr:hover td.sticky-right{background:var(--color-table-hover)}#status-table-wrap.sticky-enabled tbody tr.row-selected td.sticky-left,#status-table-wrap.sticky-enabled tbody tr.row-selected td.sticky-right{background:var(--color-table-selected)}#status-table-wrap.sticky-enabled tbody tr.row-selected:hover td.sticky-left,#status-table-wrap.sticky-enabled tbody tr.row-selected:hover td.sticky-right{background:var(--color-table-selected-hover)}.status-pill{letter-spacing:.1em;text-transform:uppercase;border:1px solid #0000;border-radius:4px;align-items:center;padding:3px 8px;font-size:10px;font-weight:700;display:inline-flex}.status-running{color:var(--color-edf-bleu-fonce);background:#1089ff1a;border-color:#1089ff40}.status-done{color:var(--color-edf-vert-fonce);background:#307a1014;border-color:#307a1033}.status-failed{color:var(--color-edf-orange-fonce);background:#d6430a14;border-color:#d6430a40}.status-converged{color:var(--color-edf-vert-fonce);background:#307a101f;border-color:#307a104d}.status-not-converged{color:var(--color-edf-orange-fonce);background:#d6430a1a;border-color:#d6430a40}.status-prepared,.status-unknown{background:var(--color-edf-gris-clair);color:var(--color-muted);border-color:var(--color-border)}.is-active{color:var(--color-edf-bleu-fonce);background:#1057c814;border-color:#1057c833;font-weight:700}#status-card:focus{outline:none}.link-btn{color:var(--color-edf-bleu-moyen);cursor:pointer;background:0 0;border:none;font-size:12px}.link-btn:hover{text-decoration:underline}#perf-table{min-width:920px}#perf-table tbody tr:hover{background:inherit}pre,.code-box{color:var(--color-edf-gris-clair);max-height:320px;font-size:12px;font-family:var(--font-mono);background:#1e1e2d;border-radius:8px;padding:10px 12px;overflow:auto}.code-box{white-space:pre}.tail-pane{white-space:normal;max-height:320px;padding:8px 10px;overflow:auto}.tail-meta{margin:2px 0 8px}.tail-line{white-space:pre;border-radius:3px;margin:0 -4px;padding:0 4px;line-height:1.45}.tail-sev-error{color:var(--color-edf-orange-clair);background:#d6430a33}.tail-sev-warn{color:var(--color-edf-orange-clair);background:#ffb21026}.tail-sev-info{color:var(--color-edf-gris-moyen)}.tail-query-hit{color:var(--color-edf-noir);background:#ffb21080;border-radius:2px;padding:0 2px}.tail-filter-error{color:var(--color-edf-orange-clair);margin-bottom:6px;font-weight:600}.tail-new{animation:2.6s ease-out tailFlash}@keyframes tailFlash{0%{background-color:#1089ff33}to{background-color:#0000}}code{background:#ff861d14;border-radius:4px;padding:2px 6px;font-size:12px}.error-list{border-radius:8px;flex-direction:column;gap:8px;max-height:320px;display:flex;overflow:auto}.error-item{background:#d6430a0a;border:1px solid #d6430a33;border-radius:8px;padding:10px 12px}.error-meta{color:var(--color-edf-orange-fonce);margin-bottom:0;font-size:11px;font-weight:700}.error-meta-row{justify-content:space-between;align-items:center;gap:10px;margin-bottom:0;display:flex}.error-meta-row+.error-line{margin-top:6px}.error-meta-row .error-meta{text-align:left;flex:1;margin:0}.error-badges{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.error-badge{color:var(--color-edf-orange-fonce);background:#d6430a14;border:1px solid #0000;border-radius:4px;align-items:center;padding:2px 7px;font-size:10px;font-weight:700;display:inline-flex}.error-badge-new{color:var(--color-edf-bleu-fonce);background:#1057c814;border-color:#1057c833}.error-line{font-family:var(--font-mono);white-space:pre-wrap;color:var(--color-edf-noir);font-size:12px}.error-empty{color:var(--color-muted);padding:6px 0;font-size:12px}.error-sev-warn{background:#ffb2100a;border-color:#ffb2104d}.error-sev-warn .error-meta,.error-sev-warn .error-line{color:var(--color-edf-orange-fonce)}.error-sev-info{background:#1089ff08;border-color:#1089ff33}.error-sev-info .error-meta,.error-sev-info .error-line{color:var(--color-edf-bleu-fonce)}.err-hit{background:#d6430a40;border-radius:2px;padding:0 2px}.error-sev-warn .err-hit{background:#ffb21059}.error-sev-info .err-hit{background:#1089ff33}.error-item-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:100%;font:inherit;color:inherit;text-align:left;background:0 0;border:none;padding:0}.error-expand-icon{color:var(--color-muted);align-items:center;margin-right:4px;display:inline-flex}@keyframes rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:980px){main>section{grid-column:span 12!important}table{min-width:640px}}@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}.icon{vertical-align:-2px;display:inline-block}div.svelte-1kfua4v svg{display:block;max-width:100%;height:auto} +/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:"JetBrains Mono", "Fira Code", "Cascadia Code", "Courier New", monospace;--color-red-600:oklch(57.7% .245 27.325);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-light:300;--font-weight-normal:400;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"edf-2020";--default-mono-font-family:var(--font-mono);--color-edf-orange-fonce:#d6430a;--color-edf-orange-moyen:#ff861d;--color-edf-orange-clair:#ffb210;--color-edf-vert-fonce:#307a10;--color-edf-bleu-fonce:#001a70;--color-edf-bleu-moyen:#1057c8;--color-edf-bleu-clair:#1089ff;--color-edf-noir:#333;--color-edf-slate:#f8f7f7;--color-edf-gris-moyen:#dedede;--color-edf-gris-clair:#f8f7f7;--color-edf-blanc:#fff;--color-bg:var(--color-edf-slate);--color-ink:var(--color-edf-noir);--color-muted:#787878;--color-card:var(--color-edf-blanc);--color-border:var(--color-edf-gris-moyen);--color-table-head:var(--color-edf-gris-clair);--color-table-row:var(--color-edf-blanc);--color-table-hover:#f5f5f4;--color-table-selected:#e8eff8;--color-table-selected-hover:#dce7f4;--animate-rise:rise .35s ease both}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.sticky\!{position:sticky!important}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.right-0{right:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-80{z-index:80}.z-\[1\]{z-index:1}.z-\[1000\]{z-index:1000}.col-span-6{grid-column:span 6/span 6}.col-span-12{grid-column:span 12/span 12}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing) * 0)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-auto{margin-right:auto}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-4\.5{margin-bottom:calc(var(--spacing) * 4.5)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[900\/500\]{aspect-ratio:900/500}.h-7{height:calc(var(--spacing) * 7)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-\[3px\]{height:3px}.h-\[10px\]{height:10px}.h-\[14px\]{height:14px}.h-\[30px\]{height:30px}.h-\[34px\]{height:34px}.h-full{height:100%}.max-h-\[240px\]{max-height:240px}.max-h-\[260px\]{max-height:260px}.max-h-\[500px\]{max-height:500px}.\!min-h-\[80px\]{min-height:80px!important}.w-9{width:calc(var(--spacing) * 9)}.w-32\.5{width:calc(var(--spacing) * 32.5)}.w-37\.5{width:calc(var(--spacing) * 37.5)}.w-\[10px\]{width:10px}.w-\[14px\]{width:14px}.w-\[30px\]{width:30px}.w-\[40px\]{width:40px}.w-\[70px\]{width:70px}.w-\[72px\]{width:72px}.w-\[80px\]{width:80px}.w-\[90px\]{width:90px}.w-\[110px\]{width:110px}.w-\[130px\]{width:130px}.w-\[140px\]{width:140px}.w-\[150px\]{width:150px}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[200px\]{width:200px}.w-\[min\(420px\,96vw\)\]{width:min(420px,96vw)}.w-\[min\(1200px\,94vw\)\]{width:min(1200px,94vw)}.w-\[min\(\{maxWidth\}\,96vw\)\]{width:min({maxWidth},96vw)}.w-auto{width:auto}.w-full{width:100%}.w-max{width:max-content}.max-w-\[140px\]{max-width:140px}.max-w-\[320px\]{max-width:320px}.max-w-\[920px\]{max-width:920px}.\!min-w-0{min-width:calc(var(--spacing) * 0)!important}.min-w-\[36px\]{min-width:36px}.min-w-\[40px\]{min-width:40px}.min-w-\[72px\]{min-width:72px}.min-w-\[170px\]{min-width:170px}.min-w-\[200px\]{min-width:200px}.min-w-full{min-width:100%}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-180{rotate:180deg}.animate-pulse{animation:var(--animate-pulse)}.animate-rise{animation:var(--animate-rise)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.self-end{align-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-\[rgba\(51\,51\,51\,0\.08\)\]{border-color:#33333314}.border-border{border-color:var(--color-border)}.border-edf-bleu-moyen{border-color:var(--color-edf-bleu-moyen)}.border-edf-gris-moyen{border-color:var(--color-edf-gris-moyen)}.border-transparent{border-color:#0000}.bg-\[rgba\(0\,26\,112\,0\.3\)\]{background-color:#001a704d}.bg-\[rgba\(16\,87\,200\,0\.06\)\]{background-color:#1057c80f}.bg-\[rgba\(48\,122\,16\,0\.04\)\]{background-color:#307a100a}.bg-\[rgba\(48\,122\,16\,0\.06\)\]{background-color:#307a100f}.bg-\[rgba\(48\,122\,16\,0\.08\)\]{background-color:#307a1014}.bg-\[rgba\(214\,67\,10\,0\.04\)\]{background-color:#d6430a0a}.bg-\[rgba\(214\,67\,10\,0\.06\)\]{background-color:#d6430a0f}.bg-\[rgba\(214\,67\,10\,0\.08\)\]{background-color:#d6430a14}.bg-\[rgba\(214\,67\,10\,0\.12\)\]{background-color:#d6430a1f}.bg-\[rgba\(255\,178\,16\,0\.1\)\]{background-color:#ffb2101a}.bg-\[rgba\(255\,178\,16\,0\.3\)\]{background-color:#ffb2104d}.bg-\[rgba\(255\,178\,16\,0\.6\)\]{background-color:#ffb21099}.bg-card{background-color:var(--color-card)}.bg-edf-blanc{background-color:var(--color-edf-blanc)}.bg-edf-bleu-moyen{background-color:var(--color-edf-bleu-moyen)}.bg-edf-gris-clair{background-color:var(--color-edf-gris-clair)}.bg-edf-orange-fonce{background-color:var(--color-edf-orange-fonce)}.bg-edf-orange-moyen{background-color:var(--color-edf-orange-moyen)}.bg-edf-vert-fonce{background-color:var(--color-edf-vert-fonce)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/60{background-color:#fff9}@supports (color:color-mix(in lab,red,red)){.bg-white\/60{background-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-1{padding:calc(var(--spacing) * 1)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-4\.5{padding:calc(var(--spacing) * 4.5)}.p-5{padding:calc(var(--spacing) * 5)}.p-\[16px_18px_18px\]{padding:16px 18px 18px}.\!px-2\.5{padding-inline:calc(var(--spacing) * 2.5)!important}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-\[1px\]{padding-inline:1px}.px-\[10px\]{padding-inline:10px}.\!py-0{padding-block:calc(var(--spacing) * 0)!important}.\!py-1{padding-block:calc(var(--spacing) * 1)!important}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-\[7px\]{padding-block:7px}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-7{padding-right:calc(var(--spacing) * 7)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[edf-2020-soft\]{font-family:edf-2020-soft}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[1\.45\]{--tw-leading:1.45;line-height:1.45}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-edf-bleu-clair{color:var(--color-edf-bleu-clair)}.text-edf-bleu-fonce{color:var(--color-edf-bleu-fonce)}.text-edf-bleu-moyen{color:var(--color-edf-bleu-moyen)}.text-edf-gris-moyen{color:var(--color-edf-gris-moyen)}.text-edf-noir{color:var(--color-edf-noir)}.text-edf-orange-fonce{color:var(--color-edf-orange-fonce)}.text-edf-vert-fonce{color:var(--color-edf-vert-fonce)}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.italic{font-style:italic}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-\[background\]{transition-property:background;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-\[120ms\]{--tw-duration:.12s;transition-duration:.12s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.hover\:border-border:hover{border-color:var(--color-border)}.hover\:border-edf-bleu-clair:hover{border-color:var(--color-edf-bleu-clair)}.hover\:border-edf-gris-moyen:hover{border-color:var(--color-edf-gris-moyen)}.hover\:bg-\[rgb\(12\,72\,170\)\]:hover{background-color:#0c48aa}.hover\:bg-\[rgb\(38\,98\,12\)\]:hover{background-color:#26620c}.hover\:bg-\[rgb\(180\,56\,8\)\]:hover{background-color:#b43808}.hover\:bg-\[rgb\(230\,118\,20\)\]:hover{background-color:#e67614}.hover\:bg-edf-gris-clair:hover{background-color:var(--color-edf-gris-clair)}.hover\:text-muted:hover{color:var(--color-muted)}.hover\:underline:hover{text-decoration-line:underline}.hover\:decoration-dotted:hover{text-decoration-style:dotted}.hover\:underline-offset-2:hover{text-underline-offset:2px}}.focus\:border-edf-bleu-moyen:focus{border-color:var(--color-edf-bleu-moyen)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}@media not all and (min-width:64rem){.max-lg\:order-3{order:3}.max-lg\:h-auto{height:auto}.max-lg\:w-full{width:100%}.max-lg\:flex-wrap{flex-wrap:wrap}.max-lg\:gap-2{gap:calc(var(--spacing) * 2)}.max-lg\:gap-3{gap:calc(var(--spacing) * 3)}}}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Regular.DEtAzL8F.woff2)format("woff2");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Bold.Bub472Sg.woff2)format("woff2");font-weight:700;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Light.D7COPh2m.woff2)format("woff2");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:edf-2020;src:url(./EDF2020W01-Italic.CtMn0wF8.woff2)format("woff2");font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-BoldItalic.y4KHcb-c.woff2)format("woff2");font-weight:700;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-Italic.CTdZcEjD.woff2)format("woff2");font-weight:400;font-style:italic;font-display:swap}@font-face{font-family:edf-2020-soft;src:url(./EDF2020Soft-LightItalic.DlUXaATs.woff2)format("woff2");font-weight:300;font-style:italic;font-display:swap}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{color:var(--color-ink);background:var(--color-bg);min-height:100vh;margin:0}input,select,textarea{border:1px solid var(--color-border);height:30px;color:var(--color-ink);background:#fff;border-radius:6px;padding:5px 10px;font-family:inherit;font-size:13px;transition:border-color .15s}textarea,select[multiple]{height:auto}input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}input:focus,select:focus,textarea:focus{border-color:var(--color-edf-bleu-moyen);outline:none}select{min-width:160px}select[multiple]{min-width:180px}textarea{width:100%;min-height:160px;font-family:var(--font-mono);font-size:12px}[data-icon]:before{content:attr(data-icon);opacity:.8;justify-content:center;align-items:center;font-size:12px;display:inline-flex}.table-wrap{border:1px solid var(--color-border);background:var(--color-table-row);border-radius:8px;overflow-x:auto}#status-table{border-collapse:separate;border-spacing:0;width:max-content;min-width:100%}table{border-collapse:collapse;width:100%;min-width:760px}th,td{text-align:left;border-bottom:1px solid var(--color-edf-gris-clair);padding:8px 10px;font-size:13px}th{text-transform:uppercase;letter-spacing:.12em;color:var(--color-muted);background:var(--color-table-head);z-index:1;font-size:11px;font-weight:700;position:sticky;top:0}th.status-col-doe,th.status-col-calc{white-space:nowrap}td.status-col-doe{font-family:var(--font-mono);font-size:12px}td.case-id{font-weight:700}tbody tr{background:var(--color-table-row)}tbody tr:hover{background:var(--color-table-hover)}tbody tr.row-selected{background:var(--color-table-selected)}tbody tr.row-selected:hover{background:var(--color-table-selected-hover)}#status-body tr,#status-body td{-webkit-user-select:none;user-select:none}th.status-sortable{cursor:pointer;-webkit-user-select:none;user-select:none}th.status-sortable:hover{color:var(--color-edf-bleu-moyen)}#status-doe-select{min-width:220px;max-width:360px}#status-table thead tr,#status-table tbody tr{height:34px}#status-table-wrap{max-height:374px;overflow-y:auto}#status-table-wrap.sticky-enabled th.sticky-left,#status-table-wrap.sticky-enabled th.sticky-right{background:var(--color-table-head);z-index:4;position:sticky}#status-table-wrap.sticky-enabled td.sticky-left,#status-table-wrap.sticky-enabled td.sticky-right{background:var(--color-table-row);z-index:3;position:sticky}#status-table-wrap.sticky-enabled .sticky-right{right:0}#status-table-wrap.sticky-enabled .sticky-divider{border-right:1px solid var(--color-border)}#status-table-wrap.sticky-enabled .sticky-right.sticky-divider{border-left:1px solid var(--color-border);border-right:none}#status-table-wrap.sticky-enabled tbody tr:hover td.sticky-left,#status-table-wrap.sticky-enabled tbody tr:hover td.sticky-right{background:var(--color-table-hover)}#status-table-wrap.sticky-enabled tbody tr.row-selected td.sticky-left,#status-table-wrap.sticky-enabled tbody tr.row-selected td.sticky-right{background:var(--color-table-selected)}#status-table-wrap.sticky-enabled tbody tr.row-selected:hover td.sticky-left,#status-table-wrap.sticky-enabled tbody tr.row-selected:hover td.sticky-right{background:var(--color-table-selected-hover)}.status-pill{letter-spacing:.1em;text-transform:uppercase;border:1px solid #0000;border-radius:4px;align-items:center;padding:3px 8px;font-size:10px;font-weight:700;display:inline-flex}.status-running{color:var(--color-edf-bleu-fonce);background:#1089ff1a;border-color:#1089ff40}.status-done{color:var(--color-edf-vert-fonce);background:#307a1014;border-color:#307a1033}.status-failed{color:var(--color-edf-orange-fonce);background:#d6430a14;border-color:#d6430a40}.status-converged{color:var(--color-edf-vert-fonce);background:#307a101f;border-color:#307a104d}.status-not-converged{color:var(--color-edf-orange-fonce);background:#d6430a1a;border-color:#d6430a40}.status-prepared,.status-unknown{background:var(--color-edf-gris-clair);color:var(--color-muted);border-color:var(--color-border)}.is-active{color:var(--color-edf-bleu-fonce);background:#1057c814;border-color:#1057c833;font-weight:700}#status-card:focus{outline:none}.link-btn{color:var(--color-edf-bleu-moyen);cursor:pointer;background:0 0;border:none;font-size:12px}.link-btn:hover{text-decoration:underline}#perf-table{min-width:920px}#perf-table tbody tr:hover{background:inherit}pre,.code-box{color:var(--color-edf-gris-clair);max-height:320px;font-size:12px;font-family:var(--font-mono);background:#1e1e2d;border-radius:8px;padding:10px 12px;overflow:auto}.code-box{white-space:pre}.tail-pane{white-space:normal;max-height:320px;padding:8px 10px;overflow:auto}.tail-meta{margin:2px 0 8px}.tail-line{white-space:pre;border-radius:3px;margin:0 -4px;padding:0 4px;line-height:1.45}.tail-sev-error{color:var(--color-edf-orange-clair);background:#d6430a33}.tail-sev-warn{color:var(--color-edf-orange-clair);background:#ffb21026}.tail-query-hit{color:var(--color-edf-noir);background:#ffb21080;border-radius:2px;padding:0 2px}.tail-filter-error{color:var(--color-edf-orange-clair);margin-bottom:6px;font-weight:600}.tail-new{animation:2.6s ease-out tailFlash}@keyframes tailFlash{0%{background-color:#1089ff33}to{background-color:#0000}}code{background:#ff861d14;border-radius:4px;padding:2px 6px;font-size:12px}.error-list{border-radius:8px;flex-direction:column;gap:8px;max-height:320px;display:flex;overflow:auto}.error-item{background:#d6430a0a;border:1px solid #d6430a33;border-radius:8px;padding:10px 12px}.error-meta{color:var(--color-edf-orange-fonce);margin-bottom:0;font-size:11px;font-weight:700}.error-meta-row{justify-content:space-between;align-items:center;gap:10px;margin-bottom:0;display:flex}.error-meta-row+.error-line{margin-top:6px}.error-meta-row .error-meta{text-align:left;flex:1;margin:0}.error-badges{flex-wrap:wrap;align-items:center;gap:6px;display:inline-flex}.error-badge{color:var(--color-edf-orange-fonce);background:#d6430a14;border:1px solid #0000;border-radius:4px;align-items:center;padding:2px 7px;font-size:10px;font-weight:700;display:inline-flex}.error-badge-new{color:var(--color-edf-bleu-fonce);background:#1057c814;border-color:#1057c833}.error-line{font-family:var(--font-mono);white-space:pre-wrap;color:var(--color-edf-noir);font-size:12px}.error-empty{color:var(--color-muted);padding:6px 0;font-size:12px}.error-sev-warn{background:#ffb2100a;border-color:#ffb2104d}.error-sev-warn .error-meta,.error-sev-warn .error-line{color:var(--color-edf-orange-fonce)}.error-sev-info{background:#1089ff08;border-color:#1089ff33}.error-sev-info .error-meta,.error-sev-info .error-line{color:var(--color-edf-bleu-fonce)}.err-hit{background:#d6430a40;border-radius:2px;padding:0 2px}.error-sev-warn .err-hit{background:#ffb21059}.error-sev-info .err-hit{background:#1089ff33}.error-item-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:100%;font:inherit;color:inherit;text-align:left;background:0 0;border:none;padding:0}.error-expand-icon{color:var(--color-muted);align-items:center;margin-right:4px;display:inline-flex}@keyframes rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:980px){main>section{grid-column:span 12!important}table{min-width:640px}}@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}.icon{vertical-align:-2px;display:inline-block}div.svelte-1kfua4v svg{display:block;max-width:100%;height:auto} diff --git a/frontend/dist/_app/immutable/chunks/B-WYSyZB.js b/frontend/dist/_app/immutable/chunks/B-WYSyZB.js new file mode 100644 index 0000000..4542e85 --- /dev/null +++ b/frontend/dist/_app/immutable/chunks/B-WYSyZB.js @@ -0,0 +1 @@ +var te=t=>{throw TypeError(t)};var je=(t,e,n)=>e.has(t)||te("Cannot "+n);var v=(t,e,n)=>(je(t,e,"read from private field"),n?n.call(t):e.get(t)),U=(t,e,n)=>e.has(t)?te("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{be as Ut,bi as Ne,aK as T,D as I,a0 as O,aJ as mt,bj as De}from"./D2EIOFXK.js";import{o as ee}from"./C1tKzvKE.js";const B=[];function qt(t,e=Ut){let n=null;const a=new Set;function r(o){if(Ne(t,o)&&(t=o,n)){const l=!B.length;for(const c of a)c[1](),B.push(c,t);if(l){for(let c=0;c{a.delete(c),a.size===0&&n&&(n(),n=null)}}return{set:r,update:i,subscribe:s}}class Vt{constructor(e,n){this.status=e,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}}class Kt{constructor(e,n){this.status=e,this.location=n}}class Bt extends Error{constructor(e,n,a){super(a),this.status=e,this.text=n}}new URL("sveltekit-internal://");function qe(t,e){return t==="/"||e==="ignore"?t:e==="never"?t.endsWith("/")?t.slice(0,-1):t:e==="always"&&!t.endsWith("/")?t+"/":t}function Ve(t){return t.split("%25").map(decodeURI).join("%25")}function Ke(t){for(const e in t)t[e]=decodeURIComponent(t[e]);return t}function Tt({href:t}){return t.split("#")[0]}function Be(...t){let e=5381;for(const n of t)if(typeof n=="string"){let a=n.length;for(;a;)e=e*33^n.charCodeAt(--a)}else if(ArrayBuffer.isView(n)){const a=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let r=a.length;for(;r;)e=e*33^a[--r]}else throw new TypeError("value must be a string or TypedArray");return(e>>>0).toString(36)}new TextEncoder;new TextDecoder;function Me(t){const e=atob(t),n=new Uint8Array(e.length);for(let a=0;a((t instanceof Request?t.method:(e==null?void 0:e.method)||"GET")!=="GET"&&H.delete(Mt(t)),ze(t,e));const H=new Map;function Fe(t,e){const n=Mt(t,e),a=document.querySelector(n);if(a!=null&&a.textContent){a.remove();let{body:r,...i}=JSON.parse(a.textContent);const s=a.getAttribute("data-ttl");return s&&H.set(n,{body:r,init:i,ttl:1e3*Number(s)}),a.getAttribute("data-b64")!==null&&(r=Me(r)),Promise.resolve(new Response(r,i))}return window.fetch(t,e)}function Ge(t,e,n){if(H.size>0){const a=Mt(t,n),r=H.get(a);if(r){if(performance.now(){const r=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(a);if(r)return e.push({name:r[1],matcher:r[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const i=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(a);if(i)return e.push({name:i[1],matcher:i[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!a)return;const s=a.split(/\[(.+?)\](?!\])/);return"/"+s.map((l,c)=>{if(c%2){if(l.startsWith("x+"))return It(String.fromCharCode(parseInt(l.slice(2),16)));if(l.startsWith("u+"))return It(String.fromCharCode(...l.slice(2).split("-").map(m=>parseInt(m,16))));const d=We.exec(l),[,u,w,p,f]=d;return e.push({name:p,matcher:f,optional:!!u,rest:!!w,chained:w?c===1&&s[0]==="":!1}),w?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return It(l)}).join("")}).join("")}/?$`),params:e}}function Ye(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function He(t){return t.slice(1).split("/").filter(Ye)}function Xe(t,e,n){const a={},r=t.slice(1),i=r.filter(o=>o!==void 0);let s=0;for(let o=0;od).join("/"),s=0),c===void 0)if(l.rest)c="";else continue;if(!l.matcher||n[l.matcher](c)){a[l.name]=c;const d=e[o+1],u=r[o+1];d&&!d.rest&&d.optional&&u&&l.chained&&(s=0),!d&&!u&&Object.keys(a).length===i.length&&(s=0);continue}if(l.optional&&l.chained){s++;continue}return}if(!s)return a}function It(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function Qe({nodes:t,server_loads:e,dictionary:n,matchers:a}){const r=new Set(e);return Object.entries(n).map(([o,[l,c,d]])=>{const{pattern:u,params:w}=Je(o),p={id:o,exec:f=>{const m=u.exec(f);if(m)return Xe(m,w,a)},errors:[1,...d||[]].map(f=>t[f]),layouts:[0,...c||[]].map(s),leaf:i(l)};return p.errors.length=p.layouts.length=Math.max(p.errors.length,p.layouts.length),p});function i(o){const l=o<0;return l&&(o=~o),[l,t[o]]}function s(o){return o===void 0?o:[r.has(o),t[o]]}}function ge(t,e=JSON.parse){try{return e(sessionStorage[t])}catch{}}function ne(t,e,n=JSON.stringify){const a=n(e);try{sessionStorage[t]=a}catch{}}var fe;const A=((fe=globalThis.__sveltekit_8dgavl)==null?void 0:fe.base)??"";var ue;const Ze=((ue=globalThis.__sveltekit_8dgavl)==null?void 0:ue.assets)??A??"",tn="1791465900433",me="sveltekit:snapshot",_e="sveltekit:scroll",we="sveltekit:states",en="sveltekit:pageurl",z="sveltekit:history",Q="sveltekit:navigation",D={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},zt=location.origin;function ve(t){if(t instanceof URL)return t;let e=document.baseURI;if(!e){const n=document.getElementsByTagName("base");e=n.length?n[0].href:document.URL}return new URL(t,e)}function V(){return{x:pageXOffset,y:pageYOffset}}function M(t,e){return t.getAttribute(`data-sveltekit-${e}`)}const re={...D,"":D.hover};function ye(t){let e=t.assignedSlot??t.parentNode;return(e==null?void 0:e.nodeType)===11&&(e=e.host),e}function be(t,e){for(;t&&t!==e;){if(t.nodeName.toUpperCase()==="A"&&t.hasAttribute("href"))return t;t=ye(t)}}function $t(t,e,n){let a;try{if(a=new URL(t instanceof SVGAElement?t.href.baseVal:t.href,document.baseURI),n&&a.hash.match(/^#[^/]/)){const o=location.hash.split("#")[1]||"/";a.hash=`#${o}${a.hash}`}}catch{}const r=t instanceof SVGAElement?t.target.baseVal:t.target,i=!a||!!r||Rt(a,e,n)||(t.getAttribute("rel")||"").split(/\s+/).includes("external"),s=(a==null?void 0:a.origin)===zt&&t.hasAttribute("download");return{url:a,external:i,target:r,download:s}}function _t(t){let e=null,n=null,a=null,r=null,i=null,s=null,o=t;for(;o&&o!==document.documentElement;)a===null&&(a=M(o,"preload-code")),r===null&&(r=M(o,"preload-data")),e===null&&(e=M(o,"keepfocus")),n===null&&(n=M(o,"noscroll")),i===null&&(i=M(o,"reload")),s===null&&(s=M(o,"replacestate")),o=ye(o);function l(c){switch(c){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:re[a??"off"],preload_data:re[r??"off"],keepfocus:l(e),noscroll:l(n),reload:l(i),replace_state:l(s)}}function ae(t){const e=qt(t);let n=!0;function a(){n=!0,e.update(s=>s)}function r(s){n=!1,e.set(s)}function i(s){let o;return e.subscribe(l=>{(o===void 0||n&&l!==o)&&s(o=l)})}return{notify:a,set:r,subscribe:i}}const ke={v:()=>{}};function nn(){const{set:t,subscribe:e}=qt(!1);let n;async function a(){clearTimeout(n);try{const r=await fetch(`${Ze}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const s=(await r.json()).version!==tn;return s&&(t(!0),ke.v(),clearTimeout(n)),s}catch{return!1}}return{subscribe:e,check:a}}function Rt(t,e,n){return t.origin!==zt||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function On(t){}const Se=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...Se];const rn=new Set([...Se]);[...rn];function an(t){return t.filter(e=>e!=null)}function Ft(t){return t instanceof Vt||t instanceof Bt?t.status:500}function on(t){return t instanceof Bt?t.text:"Internal Error"}let R,Z,Ot;const sn=ee.toString().includes("$$")||/function \w+\(\) \{\}/.test(ee.toString()),oe="a:";var at,ot,st,it,lt,ct,ft,ut,de,dt,he,ht,pe;sn?(R={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL(oe)},Z={current:null},Ot={current:!1}):(R=new(de=class{constructor(){U(this,at,T({}));U(this,ot,T(null));U(this,st,T(null));U(this,it,T({}));U(this,lt,T({id:null}));U(this,ct,T({}));U(this,ft,T(-1));U(this,ut,T(new URL(oe)))}get data(){return I(v(this,at))}set data(e){O(v(this,at),e)}get form(){return I(v(this,ot))}set form(e){O(v(this,ot),e)}get error(){return I(v(this,st))}set error(e){O(v(this,st),e)}get params(){return I(v(this,it))}set params(e){O(v(this,it),e)}get route(){return I(v(this,lt))}set route(e){O(v(this,lt),e)}get state(){return I(v(this,ct))}set state(e){O(v(this,ct),e)}get status(){return I(v(this,ft))}set status(e){O(v(this,ft),e)}get url(){return I(v(this,ut))}set url(e){O(v(this,ut),e)}},at=new WeakMap,ot=new WeakMap,st=new WeakMap,it=new WeakMap,lt=new WeakMap,ct=new WeakMap,ft=new WeakMap,ut=new WeakMap,de),Z=new(he=class{constructor(){U(this,dt,T(null))}get current(){return I(v(this,dt))}set current(e){O(v(this,dt),e)}},dt=new WeakMap,he),Ot=new(pe=class{constructor(){U(this,ht,T(!1))}get current(){return I(v(this,ht))}set current(e){O(v(this,ht),e)}},ht=new WeakMap,pe),ke.v=()=>Ot.current=!0);function ln(t){Object.assign(R,t)}const cn=new Set(["icon","shortcut icon","apple-touch-icon"]);let J=null;const j=ge(_e)??{},tt=ge(me)??{},C={url:ae({}),page:ae({}),navigating:qt(null),updated:nn()};function Gt(t){j[t]=V()}function fn(t,e){let n=t+1;for(;j[n];)delete j[n],n+=1;for(n=e+1;tt[n];)delete tt[n],n+=1}function et(t,e=!1){return e?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Ee(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(A||"/");t&&await t.update()}}function se(){}let Wt,Ct,wt,P,jt,S;const vt=[],yt=[];let y=null;function Nt(){var t;(t=y==null?void 0:y.fork)==null||t.then(e=>e==null?void 0:e.discard()),y=null}const gt=new Map,Re=new Set,un=new Set,X=new Set;let _={branch:[],error:null,url:null},xe=!1,bt=!1,ie=!0,nt=!1,Y=!1,Le=!1,Jt=!1,Ae,k,L,q;const kt=new Set,le=new Map;async function jn(t,e,n){var i,s,o,l,c;(i=globalThis.__sveltekit_8dgavl)!=null&&i.data&&globalThis.__sveltekit_8dgavl.data,document.URL!==location.href&&(location.href=location.href),S=t,await((o=(s=t.hooks).init)==null?void 0:o.call(s)),Wt=Qe(t),P=document.documentElement,jt=e,Ct=t.nodes[0],wt=t.nodes[1],Ct(),wt(),k=(l=history.state)==null?void 0:l[z],L=(c=history.state)==null?void 0:c[Q],k||(k=L=Date.now(),history.replaceState({...history.state,[z]:k,[Q]:L},""));const a=j[k];function r(){a&&(history.scrollRestoration="manual",scrollTo(a.x,a.y))}n?(r(),await Rn(jt,n)):(await F({type:"enter",url:ve(S.hash?An(new URL(location.href)):location.href),replace_state:!0}),r()),En()}function dn(){vt.length=0,Jt=!1}function Ue(t){yt.some(e=>e==null?void 0:e.snapshot)&&(tt[t]=yt.map(e=>{var n;return(n=e==null?void 0:e.snapshot)==null?void 0:n.capture()}))}function Te(t){var e;(e=tt[t])==null||e.forEach((n,a)=>{var r,i;(i=(r=yt[a])==null?void 0:r.snapshot)==null||i.restore(n)})}function ce(){Gt(k),ne(_e,j),Ue(L),ne(me,tt)}async function hn(t,e,n,a){let r;e.invalidateAll&&Nt(),await F({type:"goto",url:ve(t),keepfocus:e.keepFocus,noscroll:e.noScroll,replace_state:e.replaceState,state:e.state,redirect_count:n,nav_token:a,accept:()=>{e.invalidateAll&&(Jt=!0,r=[...le.keys()]),e.invalidate&&e.invalidate.forEach(Sn)}}),e.invalidateAll&&mt().then(mt).then(()=>{le.forEach(({resource:i},s)=>{var o;r!=null&&r.includes(s)&&((o=i.refresh)==null||o.call(i))})})}async function pn(t){if(t.id!==(y==null?void 0:y.id)){Nt();const e={};kt.add(e),y={id:t.id,token:e,promise:Oe({...t,preload:e}).then(n=>(kt.delete(e),n.type==="loaded"&&n.state.error&&Nt(),n)),fork:null}}return y.promise}async function Pt(t){var n;const e=(n=await xt(t,!1))==null?void 0:n.route;e&&await Promise.all([...e.layouts,e.leaf].filter(Boolean).map(a=>a[1]()))}async function Ie(t,e,n){var i;const a={params:_.params,route:{id:((i=_.route)==null?void 0:i.id)??null},url:new URL(location.href)};_={...t.state,nav:a};const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(R,t.props.page),Ae=new S.root({target:e,props:{...t.props,stores:C,components:yt},hydrate:n,sync:!1,transformError:void 0}),await Promise.resolve(),Te(L),n){const s={from:null,to:{...a,scroll:j[k]??V()},willUnload:!1,type:"enter",complete:Promise.resolve()};X.forEach(o=>o(s))}bt=!0}async function St({url:t,params:e,branch:n,errors:a,status:r,error:i,route:s,form:o}){let l="never";if(A&&(t.pathname===A||t.pathname===A+"/"))l="always";else for(const f of n)(f==null?void 0:f.slash)!==void 0&&(l=f.slash);t.pathname=qe(t.pathname,l),t.search=t.search;const c={type:"loaded",state:{url:t,params:e,branch:n,error:i,route:s},props:{constructors:an(n).map(f=>f.node.component),page:Zt(R)}};o!==void 0&&(c.props.form=o);let d={},u=!R,w=0;for(let f=0;fo(new URL(s))))return!0;return!1}function Ht(t,e){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?e??null:null}function _n(t,e){if(!t)return new Set(e.searchParams.keys());const n=new Set([...t.searchParams.keys(),...e.searchParams.keys()]);for(const a of n){const r=t.searchParams.getAll(a),i=e.searchParams.getAll(a);r.every(s=>i.includes(s))&&i.every(s=>r.includes(s))&&n.delete(a)}return n}function wn({error:t,url:e,route:n,params:a}){return{type:"loaded",state:{error:t,url:e,route:n,params:a,branch:[]},props:{page:Zt(R),constructors:[]}}}async function Oe({id:t,invalidating:e,url:n,params:a,route:r,preload:i}){if((y==null?void 0:y.id)===t)return kt.delete(y.token),y.promise;const{errors:s,layouts:o,leaf:l}=r,c=[...o,l];s.forEach(h=>h==null?void 0:h().catch(()=>{})),c.forEach(h=>h==null?void 0:h[1]().catch(()=>{}));const d=_.url?t!==Et(_.url):!1,u=_.route?r.id!==_.route.id:!1,w=_n(_.url,n);let p=!1;const f=c.map(async(h,g)=>{var $;if(!h)return;const b=_.branch[g];return h[1]===(b==null?void 0:b.loader)&&!mn(p,u,d,w,($=b.universal)==null?void 0:$.uses,a)?b:(p=!0,Yt({loader:h[1],url:n,params:a,route:r,parent:async()=>{var pt;const N={};for(let K=0;K{});const m=[];for(let h=0;hPromise.resolve({}),server_data_node:Ht(i)}),o={node:await wt(),loader:wt,universal:null,server:null,data:null};return St({url:n,params:r,branch:[s,o],status:t,error:e,errors:[],route:null})}catch(s){if(s instanceof Kt)return hn(new URL(s.location,location.href),{},0);throw s}}async function yn(t){const e=t.href;if(gt.has(e))return gt.get(e);let n;try{const a=(async()=>{let r=await S.hooks.reroute({url:new URL(t),fetch:async(i,s)=>gn(i,s,t).promise})??t;if(typeof r=="string"){const i=new URL(t);S.hash?i.hash=r:i.pathname=r,r=i}return r})();gt.set(e,a),n=await a}catch{gt.delete(e);return}return n}async function xt(t,e){if(t&&!Rt(t,A,S.hash)){const n=await yn(t);if(!n)return;const a=bn(n);for(const r of Wt){const i=r.exec(a);if(i)return{id:Et(t),invalidating:e,route:r,params:Ke(i),url:t}}}}function bn(t){return Ve(S.hash?t.hash.replace(/^#/,"").replace(/[?#].+/,""):t.pathname.slice(A.length))||"/"}function Et(t){return(S.hash?t.hash.replace(/^#/,""):t.pathname)+t.search}function Pe({url:t,type:e,intent:n,delta:a,event:r,scroll:i}){let s=!1;const o=Qt(_,n,t,e,i??null);a!==void 0&&(o.navigation.delta=a),r!==void 0&&(o.navigation.event=r);const l={...o.navigation,cancel:()=>{s=!0,o.reject(new Error("navigation cancelled"))}};return nt||Re.forEach(c=>c(l)),s?null:o}async function F({type:t,url:e,popped:n,keepfocus:a,noscroll:r,replace_state:i,state:s={},redirect_count:o=0,nav_token:l={},accept:c=se,block:d=se,event:u}){var K;const w=q;q=l;const p=await xt(e,!1),f=t==="enter"?Qt(_,p,e,t):Pe({url:e,type:t,delta:n==null?void 0:n.delta,intent:p,scroll:n==null?void 0:n.scroll,event:u});if(!f){d(),q===l&&(q=w);return}const m=k,h=L;c(),nt=!0,bt&&f.navigation.type!=="enter"&&C.navigating.set(Z.current=f.navigation);let g=p&&await Oe(p);if(!g){if(Rt(e,A,S.hash))return await et(e,i);g=await $e(e,{id:null},await rt(new Bt(404,"Not Found",`Not found: ${e.pathname}`),{url:e,params:{},route:{id:null}}),404,i)}if(e=(p==null?void 0:p.url)||e,q!==l)return f.reject(new Error("navigation aborted")),!1;if(g.type==="redirect"){if(o<20){await F({type:t,url:new URL(g.location,e),popped:n,keepfocus:a,noscroll:r,replace_state:i,state:s,redirect_count:o+1,nav_token:l}),f.fulfil(void 0);return}g=await Xt({status:500,error:await rt(new Error("Redirect loop"),{url:e,params:{},route:{id:null}}),url:e,route:{id:null}})}else g.props.page.status>=400&&await C.updated.check()&&(await Ee(),await et(e,i));if(dn(),Gt(m),Ue(h),g.props.page.url.pathname!==e.pathname&&(e.pathname=g.props.page.url.pathname),s=n?n.state:s,!n){const E=i?0:1,G={[z]:k+=E,[Q]:L+=E,[we]:s};(i?history.replaceState:history.pushState).call(history,G,"",e),i||fn(k,L)}const b=p&&(y==null?void 0:y.id)===p.id?y.fork:null;y=null,g.props.page.state=s;let x;if(bt){const E=(await Promise.all(Array.from(un,W=>W(f.navigation)))).filter(W=>typeof W=="function");if(E.length>0){let W=function(){E.forEach(At=>{X.delete(At)})};E.push(W),E.forEach(At=>{X.add(At)})}const G=f.navigation.to;_={...g.state,nav:{params:G.params,route:G.route,url:G.url}},g.props.page&&(g.props.page.url=e);const Lt=b&&await b;Lt?x=Lt.commit():(J=null,Ae.$set(g.props),J&&Object.assign(g.props.page,J),ln(g.props.page),x=(K=De)==null?void 0:K()),Le=!0}else await Ie(g,jt,!1);const{activeElement:$}=document;await x,await mt(),await mt();let N=null;if(ie){const E=n?n.scroll:r?V():null;E?scrollTo(E.x,E.y):(N=e.hash&&document.getElementById(Ce(e)))?N.scrollIntoView():scrollTo(0,0)}const pt=document.activeElement!==$&&document.activeElement!==document.body;!a&&!pt&&Ln(e,!N),ie=!0,g.props.page&&(J&&Object.assign(g.props.page,J),Object.assign(R,g.props.page)),nt=!1,t==="popstate"&&Te(L),f.fulfil(void 0),f.navigation.to&&(f.navigation.to.scroll=V()),X.forEach(E=>E(f.navigation)),C.navigating.set(Z.current=null)}async function $e(t,e,n,a,r){return t.origin===zt&&t.pathname===location.pathname&&!xe?await Xt({status:a,error:n,url:t,route:e}):await et(t,r)}function kn(){let t,e={element:void 0,href:void 0},n;P.addEventListener("mousemove",o=>{const l=o.target;clearTimeout(t),t=setTimeout(()=>{i(l,D.hover)},20)});function a(o){o.defaultPrevented||i(o.composedPath()[0],D.tap)}P.addEventListener("mousedown",a),P.addEventListener("touchstart",a,{passive:!0});const r=new IntersectionObserver(o=>{for(const l of o)l.isIntersecting&&(Pt(new URL(l.target.href)),r.unobserve(l.target))},{threshold:0});async function i(o,l){const c=be(o,P),d=c===e.element&&(c==null?void 0:c.href)===e.href&&l>=n;if(!c||d)return;const{url:u,external:w,download:p}=$t(c,A,S.hash);if(w||p)return;const f=_t(c),m=u&&Et(_.url)===Et(u);if(!(f.reload||m))if(l<=f.preload_data){e={element:c,href:c.href},n=D.tap;const h=await xt(u,!1);if(!h)return;pn(h)}else l<=f.preload_code&&(e={element:c,href:c.href},n=l,Pt(u))}function s(){r.disconnect();for(const o of P.querySelectorAll("a")){const{url:l,external:c,download:d}=$t(o,A,S.hash);if(c||d)continue;const u=_t(o);u.reload||(u.preload_code===D.viewport&&r.observe(o),u.preload_code===D.eager&&Pt(l))}}X.add(s),s()}function rt(t,e){if(t instanceof Vt)return t.body;const n=Ft(t),a=on(t);return S.hooks.handleError({error:t,event:e,status:n,message:a})??{message:a}}function Sn(t){if(typeof t=="function")vt.push(t);else{const{href:e}=new URL(t,location.href);vt.push(n=>n.href===e)}}function En(){var e;history.scrollRestoration="manual",addEventListener("beforeunload",n=>{let a=!1;if(ce(),!nt){const r=Qt(_,void 0,null,"leave"),i={...r.navigation,cancel:()=>{a=!0,r.reject(new Error("navigation cancelled"))}};Re.forEach(s=>s(i))}a?(n.preventDefault(),n.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&ce()}),(e=navigator.connection)!=null&&e.saveData||kn(),P.addEventListener("click",async n=>{if(n.button||n.which!==1||n.metaKey||n.ctrlKey||n.shiftKey||n.altKey||n.defaultPrevented)return;const a=be(n.composedPath()[0],P);if(!a)return;const{url:r,external:i,target:s,download:o}=$t(a,A,S.hash);if(!r)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const l=_t(a);if(!(a instanceof SVGAElement)&&r.protocol!==location.protocol&&!(r.protocol==="https:"||r.protocol==="http:")||o)return;const[d,u]=(S.hash?r.hash.replace(/^#/,""):r.href).split("#"),w=d===Tt(location);if(i||l.reload&&(!w||!u)){Pe({url:r,type:"link",event:n})?nt=!0:n.preventDefault();return}if(u!==void 0&&w){const[,p]=_.url.href.split("#");if(p===u){if(n.preventDefault(),u===""||u==="top"&&a.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const f=a.ownerDocument.getElementById(decodeURIComponent(u));f&&(f.scrollIntoView(),f.focus())}return}if(Y=!0,Gt(k),t(r),!l.replace_state)return;Y=!1}n.preventDefault(),await new Promise(p=>{requestAnimationFrame(()=>{setTimeout(p,0)}),setTimeout(p,100)}),await F({type:"link",url:r,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??r.href===location.href,event:n})}),P.addEventListener("submit",n=>{if(n.defaultPrevented)return;const a=HTMLFormElement.prototype.cloneNode.call(n.target),r=n.submitter;if(((r==null?void 0:r.formTarget)||a.target)==="_blank"||((r==null?void 0:r.formMethod)||a.method)!=="get")return;const o=new URL((r==null?void 0:r.hasAttribute("formaction"))&&(r==null?void 0:r.formAction)||a.action);if(Rt(o,A,!1))return;const l=n.target,c=_t(l);if(c.reload)return;n.preventDefault(),n.stopPropagation();const d=new FormData(l,r);o.search=new URLSearchParams(d).toString(),F({type:"form",url:o,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??o.href===location.href,event:n})}),addEventListener("popstate",async n=>{var a;if(!Dt){if((a=n.state)!=null&&a[z]){const r=n.state[z];if(q={},r===k)return;const i=j[r],s=n.state[we]??{},o=new URL(n.state[en]??location.href),l=n.state[Q],c=_.url?Tt(location)===Tt(_.url):!1;if(l===L&&(Le||c)){s!==R.state&&(R.state=s),t(o),j[k]=V(),i&&scrollTo(i.x,i.y),k=r;return}const u=r-k;await F({type:"popstate",url:o,popped:{state:s,scroll:i,delta:u},accept:()=>{k=r,L=l},block:()=>{history.go(-u)},nav_token:q,event:n})}else if(!Y){const r=new URL(location.href);t(r),S.hash&&location.reload()}}}),addEventListener("hashchange",()=>{Y&&(Y=!1,history.replaceState({...history.state,[z]:++k,[Q]:L},"",location.href))});for(const n of document.querySelectorAll("link"))cn.has(n.rel)&&(n.href=n.href);addEventListener("pageshow",n=>{n.persisted&&C.navigating.set(Z.current=null)});function t(n){_.url=R.url=n,C.page.set(Zt(R)),C.page.notify()}}async function Rn(t,{status:e=200,error:n,node_ids:a,params:r,route:i,server_route:s,data:o,form:l}){xe=!0;const c=new URL(location.href);let d;({params:r={},route:i={id:null}}=await xt(c,!1)||{}),d=Wt.find(({id:p})=>p===i.id);let u,w=!0;try{const p=a.map(async(m,h)=>{const g=o[h];return g!=null&&g.uses&&(g.uses=xn(g.uses)),Yt({loader:S.nodes[m],url:c,params:r,route:i,parent:async()=>{const b={};for(let x=0;x{const o=history.state;Dt=!0,location.replace(new URL(`#${a}`,location.href)),history.replaceState(o,"",t),e&&scrollTo(i,s),Dt=!1})}else{const i=document.body,s=i.getAttribute("tabindex");i.tabIndex=-1,i.focus({preventScroll:!0,focusVisible:!1}),s!==null?i.setAttribute("tabindex",s):i.removeAttribute("tabindex")}const r=getSelection();if(r&&r.type!=="None"){const i=[];for(let s=0;s{if(r.rangeCount===i.length){for(let s=0;s{i=u,s=w});return o.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((c=t.route)==null?void 0:c.id)??null},url:t.url,scroll:V()},to:n&&{params:(e==null?void 0:e.params)??null,route:{id:((d=e==null?void 0:e.route)==null?void 0:d.id)??null},url:n,scroll:r},willUnload:!e,type:a,complete:o},fulfil:i,reject:s}}function Zt(t){return{data:t.data,error:t.error,form:t.form,params:t.params,route:t.route,state:t.state,status:t.status,url:t.url}}function An(t){const e=new URL(t);return e.hash=decodeURIComponent(t.hash),e}function Ce(t){let e;if(S.hash){const[,,n]=t.hash.split("#",3);e=n??""}else e=t.hash.slice(1);return decodeURIComponent(e)}export{jn as a,On as l,R as p,C as s}; diff --git a/frontend/dist/_app/immutable/chunks/BI00lc_u.js b/frontend/dist/_app/immutable/chunks/BI00lc_u.js deleted file mode 100644 index 4b090d1..0000000 --- a/frontend/dist/_app/immutable/chunks/BI00lc_u.js +++ /dev/null @@ -1 +0,0 @@ -var te=t=>{throw TypeError(t)};var je=(t,e,n)=>e.has(t)||te("Cannot "+n);var v=(t,e,n)=>(je(t,e,"read from private field"),n?n.call(t):e.get(t)),U=(t,e,n)=>e.has(t)?te("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{bf as Ut,bg as Ne,aK as T,o as I,$ as O,aJ as mt,bh as qe}from"./VJhVTb1A.js";import{o as ee}from"./Cz6m6K8p.js";const B=[];function Dt(t,e=Ut){let n=null;const a=new Set;function r(o){if(Ne(t,o)&&(t=o,n)){const l=!B.length;for(const c of a)c[1](),B.push(c,t);if(l){for(let c=0;c{a.delete(c),a.size===0&&n&&(n(),n=null)}}return{set:r,update:i,subscribe:s}}class Vt{constructor(e,n){this.status=e,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}}class Kt{constructor(e,n){this.status=e,this.location=n}}class Bt extends Error{constructor(e,n,a){super(a),this.status=e,this.text=n}}new URL("sveltekit-internal://");function De(t,e){return t==="/"||e==="ignore"?t:e==="never"?t.endsWith("/")?t.slice(0,-1):t:e==="always"&&!t.endsWith("/")?t+"/":t}function Ve(t){return t.split("%25").map(decodeURI).join("%25")}function Ke(t){for(const e in t)t[e]=decodeURIComponent(t[e]);return t}function Tt({href:t}){return t.split("#")[0]}function Be(...t){let e=5381;for(const n of t)if(typeof n=="string"){let a=n.length;for(;a;)e=e*33^n.charCodeAt(--a)}else if(ArrayBuffer.isView(n)){const a=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let r=a.length;for(;r;)e=e*33^a[--r]}else throw new TypeError("value must be a string or TypedArray");return(e>>>0).toString(36)}new TextEncoder;new TextDecoder;function Me(t){const e=atob(t),n=new Uint8Array(e.length);for(let a=0;a((t instanceof Request?t.method:(e==null?void 0:e.method)||"GET")!=="GET"&&H.delete(Mt(t)),ze(t,e));const H=new Map;function Fe(t,e){const n=Mt(t,e),a=document.querySelector(n);if(a!=null&&a.textContent){a.remove();let{body:r,...i}=JSON.parse(a.textContent);const s=a.getAttribute("data-ttl");return s&&H.set(n,{body:r,init:i,ttl:1e3*Number(s)}),a.getAttribute("data-b64")!==null&&(r=Me(r)),Promise.resolve(new Response(r,i))}return window.fetch(t,e)}function Ge(t,e,n){if(H.size>0){const a=Mt(t,n),r=H.get(a);if(r){if(performance.now(){const r=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(a);if(r)return e.push({name:r[1],matcher:r[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const i=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(a);if(i)return e.push({name:i[1],matcher:i[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!a)return;const s=a.split(/\[(.+?)\](?!\])/);return"/"+s.map((l,c)=>{if(c%2){if(l.startsWith("x+"))return It(String.fromCharCode(parseInt(l.slice(2),16)));if(l.startsWith("u+"))return It(String.fromCharCode(...l.slice(2).split("-").map(m=>parseInt(m,16))));const d=We.exec(l),[,u,w,p,f]=d;return e.push({name:p,matcher:f,optional:!!u,rest:!!w,chained:w?c===1&&s[0]==="":!1}),w?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return It(l)}).join("")}).join("")}/?$`),params:e}}function Ye(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function He(t){return t.slice(1).split("/").filter(Ye)}function Xe(t,e,n){const a={},r=t.slice(1),i=r.filter(o=>o!==void 0);let s=0;for(let o=0;od).join("/"),s=0),c===void 0)if(l.rest)c="";else continue;if(!l.matcher||n[l.matcher](c)){a[l.name]=c;const d=e[o+1],u=r[o+1];d&&!d.rest&&d.optional&&u&&l.chained&&(s=0),!d&&!u&&Object.keys(a).length===i.length&&(s=0);continue}if(l.optional&&l.chained){s++;continue}return}if(!s)return a}function It(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function Qe({nodes:t,server_loads:e,dictionary:n,matchers:a}){const r=new Set(e);return Object.entries(n).map(([o,[l,c,d]])=>{const{pattern:u,params:w}=Je(o),p={id:o,exec:f=>{const m=u.exec(f);if(m)return Xe(m,w,a)},errors:[1,...d||[]].map(f=>t[f]),layouts:[0,...c||[]].map(s),leaf:i(l)};return p.errors.length=p.layouts.length=Math.max(p.errors.length,p.layouts.length),p});function i(o){const l=o<0;return l&&(o=~o),[l,t[o]]}function s(o){return o===void 0?o:[r.has(o),t[o]]}}function ge(t,e=JSON.parse){try{return e(sessionStorage[t])}catch{}}function ne(t,e,n=JSON.stringify){const a=n(e);try{sessionStorage[t]=a}catch{}}var fe;const A=((fe=globalThis.__sveltekit_cs5p4g)==null?void 0:fe.base)??"";var ue;const Ze=((ue=globalThis.__sveltekit_cs5p4g)==null?void 0:ue.assets)??A??"",tn="1789130391568",me="sveltekit:snapshot",_e="sveltekit:scroll",we="sveltekit:states",en="sveltekit:pageurl",z="sveltekit:history",Q="sveltekit:navigation",q={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},zt=location.origin;function ve(t){if(t instanceof URL)return t;let e=document.baseURI;if(!e){const n=document.getElementsByTagName("base");e=n.length?n[0].href:document.URL}return new URL(t,e)}function V(){return{x:pageXOffset,y:pageYOffset}}function M(t,e){return t.getAttribute(`data-sveltekit-${e}`)}const re={...q,"":q.hover};function ye(t){let e=t.assignedSlot??t.parentNode;return(e==null?void 0:e.nodeType)===11&&(e=e.host),e}function be(t,e){for(;t&&t!==e;){if(t.nodeName.toUpperCase()==="A"&&t.hasAttribute("href"))return t;t=ye(t)}}function $t(t,e,n){let a;try{if(a=new URL(t instanceof SVGAElement?t.href.baseVal:t.href,document.baseURI),n&&a.hash.match(/^#[^/]/)){const o=location.hash.split("#")[1]||"/";a.hash=`#${o}${a.hash}`}}catch{}const r=t instanceof SVGAElement?t.target.baseVal:t.target,i=!a||!!r||Rt(a,e,n)||(t.getAttribute("rel")||"").split(/\s+/).includes("external"),s=(a==null?void 0:a.origin)===zt&&t.hasAttribute("download");return{url:a,external:i,target:r,download:s}}function _t(t){let e=null,n=null,a=null,r=null,i=null,s=null,o=t;for(;o&&o!==document.documentElement;)a===null&&(a=M(o,"preload-code")),r===null&&(r=M(o,"preload-data")),e===null&&(e=M(o,"keepfocus")),n===null&&(n=M(o,"noscroll")),i===null&&(i=M(o,"reload")),s===null&&(s=M(o,"replacestate")),o=ye(o);function l(c){switch(c){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:re[a??"off"],preload_data:re[r??"off"],keepfocus:l(e),noscroll:l(n),reload:l(i),replace_state:l(s)}}function ae(t){const e=Dt(t);let n=!0;function a(){n=!0,e.update(s=>s)}function r(s){n=!1,e.set(s)}function i(s){let o;return e.subscribe(l=>{(o===void 0||n&&l!==o)&&s(o=l)})}return{notify:a,set:r,subscribe:i}}const ke={v:()=>{}};function nn(){const{set:t,subscribe:e}=Dt(!1);let n;async function a(){clearTimeout(n);try{const r=await fetch(`${Ze}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const s=(await r.json()).version!==tn;return s&&(t(!0),ke.v(),clearTimeout(n)),s}catch{return!1}}return{subscribe:e,check:a}}function Rt(t,e,n){return t.origin!==zt||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function On(t){}const Se=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...Se];const rn=new Set([...Se]);[...rn];function an(t){return t.filter(e=>e!=null)}function Ft(t){return t instanceof Vt||t instanceof Bt?t.status:500}function on(t){return t instanceof Bt?t.text:"Internal Error"}let R,Z,Ot;const sn=ee.toString().includes("$$")||/function \w+\(\) \{\}/.test(ee.toString()),oe="a:";var at,ot,st,it,lt,ct,ft,ut,de,dt,he,ht,pe;sn?(R={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL(oe)},Z={current:null},Ot={current:!1}):(R=new(de=class{constructor(){U(this,at,T({}));U(this,ot,T(null));U(this,st,T(null));U(this,it,T({}));U(this,lt,T({id:null}));U(this,ct,T({}));U(this,ft,T(-1));U(this,ut,T(new URL(oe)))}get data(){return I(v(this,at))}set data(e){O(v(this,at),e)}get form(){return I(v(this,ot))}set form(e){O(v(this,ot),e)}get error(){return I(v(this,st))}set error(e){O(v(this,st),e)}get params(){return I(v(this,it))}set params(e){O(v(this,it),e)}get route(){return I(v(this,lt))}set route(e){O(v(this,lt),e)}get state(){return I(v(this,ct))}set state(e){O(v(this,ct),e)}get status(){return I(v(this,ft))}set status(e){O(v(this,ft),e)}get url(){return I(v(this,ut))}set url(e){O(v(this,ut),e)}},at=new WeakMap,ot=new WeakMap,st=new WeakMap,it=new WeakMap,lt=new WeakMap,ct=new WeakMap,ft=new WeakMap,ut=new WeakMap,de),Z=new(he=class{constructor(){U(this,dt,T(null))}get current(){return I(v(this,dt))}set current(e){O(v(this,dt),e)}},dt=new WeakMap,he),Ot=new(pe=class{constructor(){U(this,ht,T(!1))}get current(){return I(v(this,ht))}set current(e){O(v(this,ht),e)}},ht=new WeakMap,pe),ke.v=()=>Ot.current=!0);function ln(t){Object.assign(R,t)}const cn=new Set(["icon","shortcut icon","apple-touch-icon"]);let J=null;const j=ge(_e)??{},tt=ge(me)??{},C={url:ae({}),page:ae({}),navigating:Dt(null),updated:nn()};function Gt(t){j[t]=V()}function fn(t,e){let n=t+1;for(;j[n];)delete j[n],n+=1;for(n=e+1;tt[n];)delete tt[n],n+=1}function et(t,e=!1){return e?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Ee(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(A||"/");t&&await t.update()}}function se(){}let Wt,Ct,wt,P,jt,S;const vt=[],yt=[];let y=null;function Nt(){var t;(t=y==null?void 0:y.fork)==null||t.then(e=>e==null?void 0:e.discard()),y=null}const gt=new Map,Re=new Set,un=new Set,X=new Set;let _={branch:[],error:null,url:null},xe=!1,bt=!1,ie=!0,nt=!1,Y=!1,Le=!1,Jt=!1,Ae,k,L,D;const kt=new Set,le=new Map;async function jn(t,e,n){var i,s,o,l,c;(i=globalThis.__sveltekit_cs5p4g)!=null&&i.data&&globalThis.__sveltekit_cs5p4g.data,document.URL!==location.href&&(location.href=location.href),S=t,await((o=(s=t.hooks).init)==null?void 0:o.call(s)),Wt=Qe(t),P=document.documentElement,jt=e,Ct=t.nodes[0],wt=t.nodes[1],Ct(),wt(),k=(l=history.state)==null?void 0:l[z],L=(c=history.state)==null?void 0:c[Q],k||(k=L=Date.now(),history.replaceState({...history.state,[z]:k,[Q]:L},""));const a=j[k];function r(){a&&(history.scrollRestoration="manual",scrollTo(a.x,a.y))}n?(r(),await Rn(jt,n)):(await F({type:"enter",url:ve(S.hash?An(new URL(location.href)):location.href),replace_state:!0}),r()),En()}function dn(){vt.length=0,Jt=!1}function Ue(t){yt.some(e=>e==null?void 0:e.snapshot)&&(tt[t]=yt.map(e=>{var n;return(n=e==null?void 0:e.snapshot)==null?void 0:n.capture()}))}function Te(t){var e;(e=tt[t])==null||e.forEach((n,a)=>{var r,i;(i=(r=yt[a])==null?void 0:r.snapshot)==null||i.restore(n)})}function ce(){Gt(k),ne(_e,j),Ue(L),ne(me,tt)}async function hn(t,e,n,a){let r;e.invalidateAll&&Nt(),await F({type:"goto",url:ve(t),keepfocus:e.keepFocus,noscroll:e.noScroll,replace_state:e.replaceState,state:e.state,redirect_count:n,nav_token:a,accept:()=>{e.invalidateAll&&(Jt=!0,r=[...le.keys()]),e.invalidate&&e.invalidate.forEach(Sn)}}),e.invalidateAll&&mt().then(mt).then(()=>{le.forEach(({resource:i},s)=>{var o;r!=null&&r.includes(s)&&((o=i.refresh)==null||o.call(i))})})}async function pn(t){if(t.id!==(y==null?void 0:y.id)){Nt();const e={};kt.add(e),y={id:t.id,token:e,promise:Oe({...t,preload:e}).then(n=>(kt.delete(e),n.type==="loaded"&&n.state.error&&Nt(),n)),fork:null}}return y.promise}async function Pt(t){var n;const e=(n=await xt(t,!1))==null?void 0:n.route;e&&await Promise.all([...e.layouts,e.leaf].filter(Boolean).map(a=>a[1]()))}async function Ie(t,e,n){var i;const a={params:_.params,route:{id:((i=_.route)==null?void 0:i.id)??null},url:new URL(location.href)};_={...t.state,nav:a};const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(R,t.props.page),Ae=new S.root({target:e,props:{...t.props,stores:C,components:yt},hydrate:n,sync:!1,transformError:void 0}),await Promise.resolve(),Te(L),n){const s={from:null,to:{...a,scroll:j[k]??V()},willUnload:!1,type:"enter",complete:Promise.resolve()};X.forEach(o=>o(s))}bt=!0}async function St({url:t,params:e,branch:n,errors:a,status:r,error:i,route:s,form:o}){let l="never";if(A&&(t.pathname===A||t.pathname===A+"/"))l="always";else for(const f of n)(f==null?void 0:f.slash)!==void 0&&(l=f.slash);t.pathname=De(t.pathname,l),t.search=t.search;const c={type:"loaded",state:{url:t,params:e,branch:n,error:i,route:s},props:{constructors:an(n).map(f=>f.node.component),page:Zt(R)}};o!==void 0&&(c.props.form=o);let d={},u=!R,w=0;for(let f=0;fo(new URL(s))))return!0;return!1}function Ht(t,e){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?e??null:null}function _n(t,e){if(!t)return new Set(e.searchParams.keys());const n=new Set([...t.searchParams.keys(),...e.searchParams.keys()]);for(const a of n){const r=t.searchParams.getAll(a),i=e.searchParams.getAll(a);r.every(s=>i.includes(s))&&i.every(s=>r.includes(s))&&n.delete(a)}return n}function wn({error:t,url:e,route:n,params:a}){return{type:"loaded",state:{error:t,url:e,route:n,params:a,branch:[]},props:{page:Zt(R),constructors:[]}}}async function Oe({id:t,invalidating:e,url:n,params:a,route:r,preload:i}){if((y==null?void 0:y.id)===t)return kt.delete(y.token),y.promise;const{errors:s,layouts:o,leaf:l}=r,c=[...o,l];s.forEach(h=>h==null?void 0:h().catch(()=>{})),c.forEach(h=>h==null?void 0:h[1]().catch(()=>{}));const d=_.url?t!==Et(_.url):!1,u=_.route?r.id!==_.route.id:!1,w=_n(_.url,n);let p=!1;const f=c.map(async(h,g)=>{var $;if(!h)return;const b=_.branch[g];return h[1]===(b==null?void 0:b.loader)&&!mn(p,u,d,w,($=b.universal)==null?void 0:$.uses,a)?b:(p=!0,Yt({loader:h[1],url:n,params:a,route:r,parent:async()=>{var pt;const N={};for(let K=0;K{});const m=[];for(let h=0;hPromise.resolve({}),server_data_node:Ht(i)}),o={node:await wt(),loader:wt,universal:null,server:null,data:null};return St({url:n,params:r,branch:[s,o],status:t,error:e,errors:[],route:null})}catch(s){if(s instanceof Kt)return hn(new URL(s.location,location.href),{},0);throw s}}async function yn(t){const e=t.href;if(gt.has(e))return gt.get(e);let n;try{const a=(async()=>{let r=await S.hooks.reroute({url:new URL(t),fetch:async(i,s)=>gn(i,s,t).promise})??t;if(typeof r=="string"){const i=new URL(t);S.hash?i.hash=r:i.pathname=r,r=i}return r})();gt.set(e,a),n=await a}catch{gt.delete(e);return}return n}async function xt(t,e){if(t&&!Rt(t,A,S.hash)){const n=await yn(t);if(!n)return;const a=bn(n);for(const r of Wt){const i=r.exec(a);if(i)return{id:Et(t),invalidating:e,route:r,params:Ke(i),url:t}}}}function bn(t){return Ve(S.hash?t.hash.replace(/^#/,"").replace(/[?#].+/,""):t.pathname.slice(A.length))||"/"}function Et(t){return(S.hash?t.hash.replace(/^#/,""):t.pathname)+t.search}function Pe({url:t,type:e,intent:n,delta:a,event:r,scroll:i}){let s=!1;const o=Qt(_,n,t,e,i??null);a!==void 0&&(o.navigation.delta=a),r!==void 0&&(o.navigation.event=r);const l={...o.navigation,cancel:()=>{s=!0,o.reject(new Error("navigation cancelled"))}};return nt||Re.forEach(c=>c(l)),s?null:o}async function F({type:t,url:e,popped:n,keepfocus:a,noscroll:r,replace_state:i,state:s={},redirect_count:o=0,nav_token:l={},accept:c=se,block:d=se,event:u}){var K;const w=D;D=l;const p=await xt(e,!1),f=t==="enter"?Qt(_,p,e,t):Pe({url:e,type:t,delta:n==null?void 0:n.delta,intent:p,scroll:n==null?void 0:n.scroll,event:u});if(!f){d(),D===l&&(D=w);return}const m=k,h=L;c(),nt=!0,bt&&f.navigation.type!=="enter"&&C.navigating.set(Z.current=f.navigation);let g=p&&await Oe(p);if(!g){if(Rt(e,A,S.hash))return await et(e,i);g=await $e(e,{id:null},await rt(new Bt(404,"Not Found",`Not found: ${e.pathname}`),{url:e,params:{},route:{id:null}}),404,i)}if(e=(p==null?void 0:p.url)||e,D!==l)return f.reject(new Error("navigation aborted")),!1;if(g.type==="redirect"){if(o<20){await F({type:t,url:new URL(g.location,e),popped:n,keepfocus:a,noscroll:r,replace_state:i,state:s,redirect_count:o+1,nav_token:l}),f.fulfil(void 0);return}g=await Xt({status:500,error:await rt(new Error("Redirect loop"),{url:e,params:{},route:{id:null}}),url:e,route:{id:null}})}else g.props.page.status>=400&&await C.updated.check()&&(await Ee(),await et(e,i));if(dn(),Gt(m),Ue(h),g.props.page.url.pathname!==e.pathname&&(e.pathname=g.props.page.url.pathname),s=n?n.state:s,!n){const E=i?0:1,G={[z]:k+=E,[Q]:L+=E,[we]:s};(i?history.replaceState:history.pushState).call(history,G,"",e),i||fn(k,L)}const b=p&&(y==null?void 0:y.id)===p.id?y.fork:null;y=null,g.props.page.state=s;let x;if(bt){const E=(await Promise.all(Array.from(un,W=>W(f.navigation)))).filter(W=>typeof W=="function");if(E.length>0){let W=function(){E.forEach(At=>{X.delete(At)})};E.push(W),E.forEach(At=>{X.add(At)})}const G=f.navigation.to;_={...g.state,nav:{params:G.params,route:G.route,url:G.url}},g.props.page&&(g.props.page.url=e);const Lt=b&&await b;Lt?x=Lt.commit():(J=null,Ae.$set(g.props),J&&Object.assign(g.props.page,J),ln(g.props.page),x=(K=qe)==null?void 0:K()),Le=!0}else await Ie(g,jt,!1);const{activeElement:$}=document;await x,await mt(),await mt();let N=null;if(ie){const E=n?n.scroll:r?V():null;E?scrollTo(E.x,E.y):(N=e.hash&&document.getElementById(Ce(e)))?N.scrollIntoView():scrollTo(0,0)}const pt=document.activeElement!==$&&document.activeElement!==document.body;!a&&!pt&&Ln(e,!N),ie=!0,g.props.page&&(J&&Object.assign(g.props.page,J),Object.assign(R,g.props.page)),nt=!1,t==="popstate"&&Te(L),f.fulfil(void 0),f.navigation.to&&(f.navigation.to.scroll=V()),X.forEach(E=>E(f.navigation)),C.navigating.set(Z.current=null)}async function $e(t,e,n,a,r){return t.origin===zt&&t.pathname===location.pathname&&!xe?await Xt({status:a,error:n,url:t,route:e}):await et(t,r)}function kn(){let t,e={element:void 0,href:void 0},n;P.addEventListener("mousemove",o=>{const l=o.target;clearTimeout(t),t=setTimeout(()=>{i(l,q.hover)},20)});function a(o){o.defaultPrevented||i(o.composedPath()[0],q.tap)}P.addEventListener("mousedown",a),P.addEventListener("touchstart",a,{passive:!0});const r=new IntersectionObserver(o=>{for(const l of o)l.isIntersecting&&(Pt(new URL(l.target.href)),r.unobserve(l.target))},{threshold:0});async function i(o,l){const c=be(o,P),d=c===e.element&&(c==null?void 0:c.href)===e.href&&l>=n;if(!c||d)return;const{url:u,external:w,download:p}=$t(c,A,S.hash);if(w||p)return;const f=_t(c),m=u&&Et(_.url)===Et(u);if(!(f.reload||m))if(l<=f.preload_data){e={element:c,href:c.href},n=q.tap;const h=await xt(u,!1);if(!h)return;pn(h)}else l<=f.preload_code&&(e={element:c,href:c.href},n=l,Pt(u))}function s(){r.disconnect();for(const o of P.querySelectorAll("a")){const{url:l,external:c,download:d}=$t(o,A,S.hash);if(c||d)continue;const u=_t(o);u.reload||(u.preload_code===q.viewport&&r.observe(o),u.preload_code===q.eager&&Pt(l))}}X.add(s),s()}function rt(t,e){if(t instanceof Vt)return t.body;const n=Ft(t),a=on(t);return S.hooks.handleError({error:t,event:e,status:n,message:a})??{message:a}}function Sn(t){if(typeof t=="function")vt.push(t);else{const{href:e}=new URL(t,location.href);vt.push(n=>n.href===e)}}function En(){var e;history.scrollRestoration="manual",addEventListener("beforeunload",n=>{let a=!1;if(ce(),!nt){const r=Qt(_,void 0,null,"leave"),i={...r.navigation,cancel:()=>{a=!0,r.reject(new Error("navigation cancelled"))}};Re.forEach(s=>s(i))}a?(n.preventDefault(),n.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&ce()}),(e=navigator.connection)!=null&&e.saveData||kn(),P.addEventListener("click",async n=>{if(n.button||n.which!==1||n.metaKey||n.ctrlKey||n.shiftKey||n.altKey||n.defaultPrevented)return;const a=be(n.composedPath()[0],P);if(!a)return;const{url:r,external:i,target:s,download:o}=$t(a,A,S.hash);if(!r)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const l=_t(a);if(!(a instanceof SVGAElement)&&r.protocol!==location.protocol&&!(r.protocol==="https:"||r.protocol==="http:")||o)return;const[d,u]=(S.hash?r.hash.replace(/^#/,""):r.href).split("#"),w=d===Tt(location);if(i||l.reload&&(!w||!u)){Pe({url:r,type:"link",event:n})?nt=!0:n.preventDefault();return}if(u!==void 0&&w){const[,p]=_.url.href.split("#");if(p===u){if(n.preventDefault(),u===""||u==="top"&&a.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const f=a.ownerDocument.getElementById(decodeURIComponent(u));f&&(f.scrollIntoView(),f.focus())}return}if(Y=!0,Gt(k),t(r),!l.replace_state)return;Y=!1}n.preventDefault(),await new Promise(p=>{requestAnimationFrame(()=>{setTimeout(p,0)}),setTimeout(p,100)}),await F({type:"link",url:r,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??r.href===location.href,event:n})}),P.addEventListener("submit",n=>{if(n.defaultPrevented)return;const a=HTMLFormElement.prototype.cloneNode.call(n.target),r=n.submitter;if(((r==null?void 0:r.formTarget)||a.target)==="_blank"||((r==null?void 0:r.formMethod)||a.method)!=="get")return;const o=new URL((r==null?void 0:r.hasAttribute("formaction"))&&(r==null?void 0:r.formAction)||a.action);if(Rt(o,A,!1))return;const l=n.target,c=_t(l);if(c.reload)return;n.preventDefault(),n.stopPropagation();const d=new FormData(l,r);o.search=new URLSearchParams(d).toString(),F({type:"form",url:o,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??o.href===location.href,event:n})}),addEventListener("popstate",async n=>{var a;if(!qt){if((a=n.state)!=null&&a[z]){const r=n.state[z];if(D={},r===k)return;const i=j[r],s=n.state[we]??{},o=new URL(n.state[en]??location.href),l=n.state[Q],c=_.url?Tt(location)===Tt(_.url):!1;if(l===L&&(Le||c)){s!==R.state&&(R.state=s),t(o),j[k]=V(),i&&scrollTo(i.x,i.y),k=r;return}const u=r-k;await F({type:"popstate",url:o,popped:{state:s,scroll:i,delta:u},accept:()=>{k=r,L=l},block:()=>{history.go(-u)},nav_token:D,event:n})}else if(!Y){const r=new URL(location.href);t(r),S.hash&&location.reload()}}}),addEventListener("hashchange",()=>{Y&&(Y=!1,history.replaceState({...history.state,[z]:++k,[Q]:L},"",location.href))});for(const n of document.querySelectorAll("link"))cn.has(n.rel)&&(n.href=n.href);addEventListener("pageshow",n=>{n.persisted&&C.navigating.set(Z.current=null)});function t(n){_.url=R.url=n,C.page.set(Zt(R)),C.page.notify()}}async function Rn(t,{status:e=200,error:n,node_ids:a,params:r,route:i,server_route:s,data:o,form:l}){xe=!0;const c=new URL(location.href);let d;({params:r={},route:i={id:null}}=await xt(c,!1)||{}),d=Wt.find(({id:p})=>p===i.id);let u,w=!0;try{const p=a.map(async(m,h)=>{const g=o[h];return g!=null&&g.uses&&(g.uses=xn(g.uses)),Yt({loader:S.nodes[m],url:c,params:r,route:i,parent:async()=>{const b={};for(let x=0;x{const o=history.state;qt=!0,location.replace(new URL(`#${a}`,location.href)),history.replaceState(o,"",t),e&&scrollTo(i,s),qt=!1})}else{const i=document.body,s=i.getAttribute("tabindex");i.tabIndex=-1,i.focus({preventScroll:!0,focusVisible:!1}),s!==null?i.setAttribute("tabindex",s):i.removeAttribute("tabindex")}const r=getSelection();if(r&&r.type!=="None"){const i=[];for(let s=0;s{if(r.rangeCount===i.length){for(let s=0;s{i=u,s=w});return o.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((c=t.route)==null?void 0:c.id)??null},url:t.url,scroll:V()},to:n&&{params:(e==null?void 0:e.params)??null,route:{id:((d=e==null?void 0:e.route)==null?void 0:d.id)??null},url:n,scroll:r},willUnload:!e,type:a,complete:o},fulfil:i,reject:s}}function Zt(t){return{data:t.data,error:t.error,form:t.form,params:t.params,route:t.route,state:t.state,status:t.status,url:t.url}}function An(t){const e=new URL(t);return e.hash=decodeURIComponent(t.hash),e}function Ce(t){let e;if(S.hash){const[,,n]=t.hash.split("#",3);e=n??""}else e=t.hash.slice(1);return decodeURIComponent(e)}export{jn as a,On as l,R as p,C as s}; diff --git a/frontend/dist/_app/immutable/chunks/BeON-0X8.js b/frontend/dist/_app/immutable/chunks/BeON-0X8.js deleted file mode 100644 index 3c59a0f..0000000 --- a/frontend/dist/_app/immutable/chunks/BeON-0X8.js +++ /dev/null @@ -1,2 +0,0 @@ -var Oe=Object.defineProperty;var he=t=>{throw TypeError(t)};var Fe=(t,e,r)=>e in t?Oe(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var q=(t,e,r)=>Fe(t,typeof e!="symbol"?e+"":e,r),re=(t,e,r)=>e.has(t)||he("Cannot "+r);var s=(t,e,r)=>(re(t,e,"read from private field"),r?r.call(t):e.get(t)),u=(t,e,r)=>e.has(t)?he("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),n=(t,e,r,a)=>(re(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),p=(t,e,r)=>(re(t,e,"access private method"),r);import{ac as Le,o as be,U as Ie,a7 as Ee,k as Me,ad as de,ae as H,f as L,h as I,F as V,af as _e,b as xe,J as Ce,ag as Pe,ah as pe,a as F,c as me,p as se,e as ge,m as Ve,ai as Be,a9 as X,aj as K,ak as ve,al as We,am as He,an as we,i as Te,ao as Ye,d as ie,L as Z,ap as je,O as ze,aq as $,E as qe,ar as $e,as as Ge,at as Ue,au as Je,av as Qe,aw as Xe,ax as ae,G as Ke,ay as Se,Q as Ze,az as et,aA as ne,P as G,aB as tt,aC as rt,aD as st,aE as it,x as at,aF as nt,aG as ot,z as ft}from"./VJhVTb1A.js";import{b as lt}from"./C14P8jaG.js";function ct(t){let e=0,r=Ee(0),a;return()=>{Le()&&(be(r),Ie(()=>(e===0&&(a=Me(()=>t(()=>de(r)))),e+=1,()=>{H(()=>{e-=1,e===0&&(a==null||a(),a=void 0,de(r))})})))}}var ut=qe|$e;function ht(t,e,r,a){new dt(t,e,r,a)}var E,Y,T,x,g,S,m,w,k,C,N,B,j,z,A,ee,l,ke,Ae,Re,oe,J,Q,fe;class dt{constructor(e,r,a,f){u(this,l);q(this,"parent");q(this,"is_pending",!1);q(this,"transform_error");u(this,E);u(this,Y,I?L:null);u(this,T);u(this,x);u(this,g);u(this,S,null);u(this,m,null);u(this,w,null);u(this,k,null);u(this,C,0);u(this,N,0);u(this,B,!1);u(this,j,new Set);u(this,z,new Set);u(this,A,null);u(this,ee,ct(()=>(n(this,A,Ee(s(this,C))),()=>{n(this,A,null)})));var i;n(this,E,e),n(this,T,r),n(this,x,o=>{var d=V;d.b=this,d.f|=_e,a(o)}),this.parent=V.b,this.transform_error=f??((i=this.parent)==null?void 0:i.transform_error)??(o=>o),n(this,g,xe(()=>{if(I){const o=s(this,Y);Ce();const d=o.data===Pe;if(o.data.startsWith(pe)){const h=JSON.parse(o.data.slice(pe.length));p(this,l,Ae).call(this,h)}else d?p(this,l,Re).call(this):p(this,l,ke).call(this)}else p(this,l,oe).call(this)},ut)),I&&n(this,E,L)}defer_effect(e){Be(e,s(this,j),s(this,z))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!s(this,T).pending}update_pending_count(e,r){p(this,l,fe).call(this,e,r),n(this,C,s(this,C)+e),!(!s(this,A)||s(this,B))&&(n(this,B,!0),H(()=>{n(this,B,!1),s(this,A)&&Ye(s(this,A),s(this,C))}))}get_effect_pending(){return s(this,ee).call(this),be(s(this,A))}error(e){var r=s(this,T).onerror;let a=s(this,T).failed;if(!r&&!a)throw e;s(this,S)&&(ie(s(this,S)),n(this,S,null)),s(this,m)&&(ie(s(this,m)),n(this,m,null)),s(this,w)&&(ie(s(this,w)),n(this,w,null)),I&&(Z(s(this,Y)),je(),Z(ze()));var f=!1,i=!1;const o=()=>{if(f){Ue();return}f=!0,i&&Ge(),s(this,w)!==null&&se(s(this,w),()=>{n(this,w,null)}),p(this,l,Q).call(this,()=>{p(this,l,oe).call(this)})},d=c=>{try{i=!0,r==null||r(c,o),i=!1}catch(h){$(h,s(this,g)&&s(this,g).parent)}a&&n(this,w,p(this,l,Q).call(this,()=>{try{return F(()=>{var h=V;h.b=this,h.f|=_e,a(s(this,E),()=>c,()=>o)})}catch(h){return $(h,s(this,g).parent),null}}))};H(()=>{var c;try{c=this.transform_error(e)}catch(h){$(h,s(this,g)&&s(this,g).parent);return}c!==null&&typeof c=="object"&&typeof c.then=="function"?c.then(d,h=>$(h,s(this,g)&&s(this,g).parent)):d(c)})}}E=new WeakMap,Y=new WeakMap,T=new WeakMap,x=new WeakMap,g=new WeakMap,S=new WeakMap,m=new WeakMap,w=new WeakMap,k=new WeakMap,C=new WeakMap,N=new WeakMap,B=new WeakMap,j=new WeakMap,z=new WeakMap,A=new WeakMap,ee=new WeakMap,l=new WeakSet,ke=function(){try{n(this,S,F(()=>s(this,x).call(this,s(this,E))))}catch(e){this.error(e)}},Ae=function(e){const r=s(this,T).failed;r&&n(this,w,F(()=>{r(s(this,E),()=>e,()=>()=>{})}))},Re=function(){const e=s(this,T).pending;e&&(this.is_pending=!0,n(this,m,F(()=>e(s(this,E)))),H(()=>{var r=n(this,k,document.createDocumentFragment()),a=me();r.append(a),n(this,S,p(this,l,Q).call(this,()=>F(()=>s(this,x).call(this,a)))),s(this,N)===0&&(s(this,E).before(r),n(this,k,null),se(s(this,m),()=>{n(this,m,null)}),p(this,l,J).call(this,ge))}))},oe=function(){try{if(this.is_pending=this.has_pending_snippet(),n(this,N,0),n(this,C,0),n(this,S,F(()=>{s(this,x).call(this,s(this,E))})),s(this,N)>0){var e=n(this,k,document.createDocumentFragment());Ve(s(this,S),e);const r=s(this,T).pending;n(this,m,F(()=>r(s(this,E))))}else p(this,l,J).call(this,ge)}catch(r){this.error(r)}},J=function(e){this.is_pending=!1,e.transfer_effects(s(this,j),s(this,z))},Q=function(e){var r=V,a=we,f=Te;X(s(this,g)),K(s(this,g)),ve(s(this,g).ctx);try{return We.ensure(),e()}catch(i){return He(i),null}finally{X(r),K(a),ve(f)}},fe=function(e,r){var a;if(!this.has_pending_snippet()){this.parent&&p(a=this.parent,l,fe).call(a,e,r);return}n(this,N,s(this,N)+e),s(this,N)===0&&(p(this,l,J).call(this,r),s(this,m)&&se(s(this,m),()=>{n(this,m,null)}),s(this,k)&&(s(this,E).before(s(this,k)),n(this,k,null)))};function St(t){return t.endsWith("capture")&&t!=="gotpointercapture"&&t!=="lostpointercapture"}const _t=["beforeinput","click","change","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"];function kt(t){return _t.includes(t)}const pt={formnovalidate:"formNoValidate",ismap:"isMap",nomodule:"noModule",playsinline:"playsInline",readonly:"readOnly",defaultvalue:"defaultValue",defaultchecked:"defaultChecked",srcobject:"srcObject",novalidate:"noValidate",allowfullscreen:"allowFullscreen",disablepictureinpicture:"disablePictureInPicture",disableremoteplayback:"disableRemotePlayback"};function At(t){return t=t.toLowerCase(),pt[t]??t}const gt=["touchstart","touchmove"];function vt(t){return gt.includes(t)}const yt=["textarea","script","style","title"];function Rt(t){return yt.includes(t)}const M=Symbol("events"),Ne=new Set,le=new Set;function bt(t,e,r,a={}){function f(i){if(a.capture||ce.call(e,i),!i.cancelBubble)return Qe(()=>r==null?void 0:r.call(this,i))}return t.startsWith("pointer")||t.startsWith("touch")||t==="wheel"?H(()=>{e.addEventListener(t,f,a)}):e.addEventListener(t,f,a),f}function Nt(t,e,r,a,f){var i={capture:a,passive:f},o=bt(t,e,r,i);(e===document.body||e===window||e===document||e instanceof HTMLMediaElement)&&Xe(()=>{e.removeEventListener(t,o,i)})}function Dt(t,e,r){(e[M]??(e[M]={}))[t]=r}function Ot(t){for(var e=0;e{throw O});throw R}}finally{t[M]=e,delete t.currentTarget,K(P),X(W)}}}function Ft(t,e){var r=e==null?"":typeof e=="object"?`${e}`:e;r!==(t.__t??(t.__t=t.nodeValue))&&(t.__t=r,t.nodeValue=`${r}`)}function Et(t,e){return De(t,e)}function Lt(t,e){ae(),e.intro=e.intro??!1;const r=e.target,a=I,f=L;try{for(var i=Ke(r);i&&(i.nodeType!==Se||i.data!==Ze);)i=et(i);if(!i)throw ne;G(!0),Z(i);const o=De(t,{...e,anchor:i});return G(!1),o}catch(o){if(o instanceof Error&&o.message.split(` -`).some(d=>d.startsWith("https://svelte.dev/e/")))throw o;return o!==ne&&console.warn("Failed to hydrate: ",o),e.recover===!1&&tt(),ae(),rt(r),G(!1),Et(t,e)}finally{G(a),Z(f)}}const U=new Map;function De(t,{target:e,anchor:r,props:a={},events:f,context:i,intro:o=!0,transformError:d}){ae();var c=void 0,h=st(()=>{var P=r??e.appendChild(me());ht(P,{pending:()=>{}},v=>{at({});var _=Te;if(i&&(_.c=i),f&&(a.$$events=f),I&<(v,null),c=t(v,a)||{},I&&(V.nodes.end=L,L===null||L.nodeType!==Se||L.data!==nt))throw ot(),ne;ft()},d);var W=new Set,R=v=>{for(var _=0;_{var D;for(var v of W)for(const b of[e,document]){var _=U.get(b),y=_.get(v);--y==0?(b.removeEventListener(v,ce),_.delete(v),_.size===0&&U.delete(b)):_.set(v,y)}le.delete(R),P!==r&&((D=P.parentNode)==null||D.removeChild(P))}});return ue.set(c,h),c}let ue=new WeakMap;function It(t,e){const r=ue.get(t);return r?(ue.delete(t),r(e)):Promise.resolve()}export{St as a,Ot as b,bt as c,Dt as d,kt as e,Nt as f,Lt as h,Rt as i,Et as m,At as n,Ft as s,It as u}; diff --git a/frontend/dist/_app/immutable/chunks/C1tKzvKE.js b/frontend/dist/_app/immutable/chunks/C1tKzvKE.js new file mode 100644 index 0000000..f80d80f --- /dev/null +++ b/frontend/dist/_app/immutable/chunks/C1tKzvKE.js @@ -0,0 +1 @@ +import{l as e,u as a,g as t,i as c,j as u}from"./D2EIOFXK.js";function l(n){t===null&&e(),c&&t.l!==null?i(t).m.push(n):a(()=>{const o=u(n);if(typeof o=="function")return o})}function f(n){t===null&&e(),l(()=>()=>u(n))}function i(n){var o=n.l;return o.u??(o.u={a:[],b:[],m:[]})}export{f as a,l as o}; diff --git a/frontend/dist/_app/immutable/chunks/C4rlClDa.js b/frontend/dist/_app/immutable/chunks/C4rlClDa.js new file mode 100644 index 0000000..90d1090 --- /dev/null +++ b/frontend/dist/_app/immutable/chunks/C4rlClDa.js @@ -0,0 +1,2 @@ +var Oe=Object.defineProperty;var he=t=>{throw TypeError(t)};var Fe=(t,e,r)=>e in t?Oe(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var z=(t,e,r)=>Fe(t,typeof e!="symbol"?e+"":e,r),re=(t,e,r)=>e.has(t)||he("Cannot "+r);var s=(t,e,r)=>(re(t,e,"read from private field"),r?r.call(t):e.get(t)),u=(t,e,r)=>e.has(t)?he("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),n=(t,e,r,a)=>(re(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),p=(t,e,r)=>(re(t,e,"access private method"),r);import{aa as Ie,D as be,V as Le,ab as Ee,j as Me,ac as de,ad as j,f as I,h as L,o as P,ae as _e,b as Ve,w as xe,af as Ce,ag as pe,a as F,c as me,p as se,e as ge,m as Pe,ah as Be,ai as Q,aj as X,ak as ve,al as We,am as je,an as we,g as Te,ao as He,d as ie,y as Z,ap as Ye,P as qe,aq as $,E as ze,ar as $e,as as Ge,at as Ue,au as Je,av as Ke,aw as Qe,ax as ae,q as Xe,ay as Se,S as Ze,az as et,aA as ne,Q as G,aB as tt,aC as rt,aD as st,aE as it,I as at,aF as nt,aG as ot,K as ft}from"./D2EIOFXK.js";import{b as lt}from"./IT17IssP.js";function ct(t){let e=0,r=Ee(0),a;return()=>{Ie()&&(be(r),Le(()=>(e===0&&(a=Me(()=>t(()=>de(r)))),e+=1,()=>{j(()=>{e-=1,e===0&&(a==null||a(),a=void 0,de(r))})})))}}var ut=ze|$e;function ht(t,e,r,a){new dt(t,e,r,a)}var E,H,T,V,g,S,m,w,A,x,N,B,Y,q,R,ee,l,Ae,Re,ke,oe,J,K,fe;class dt{constructor(e,r,a,f){u(this,l);z(this,"parent");z(this,"is_pending",!1);z(this,"transform_error");u(this,E);u(this,H,L?I:null);u(this,T);u(this,V);u(this,g);u(this,S,null);u(this,m,null);u(this,w,null);u(this,A,null);u(this,x,0);u(this,N,0);u(this,B,!1);u(this,Y,new Set);u(this,q,new Set);u(this,R,null);u(this,ee,ct(()=>(n(this,R,Ee(s(this,x))),()=>{n(this,R,null)})));var i;n(this,E,e),n(this,T,r),n(this,V,o=>{var d=P;d.b=this,d.f|=_e,a(o)}),this.parent=P.b,this.transform_error=f??((i=this.parent)==null?void 0:i.transform_error)??(o=>o),n(this,g,Ve(()=>{if(L){const o=s(this,H);xe();const d=o.data===Ce;if(o.data.startsWith(pe)){const h=JSON.parse(o.data.slice(pe.length));p(this,l,Re).call(this,h)}else d?p(this,l,ke).call(this):p(this,l,Ae).call(this)}else p(this,l,oe).call(this)},ut)),L&&n(this,E,I)}defer_effect(e){Be(e,s(this,Y),s(this,q))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!s(this,T).pending}update_pending_count(e,r){p(this,l,fe).call(this,e,r),n(this,x,s(this,x)+e),!(!s(this,R)||s(this,B))&&(n(this,B,!0),j(()=>{n(this,B,!1),s(this,R)&&He(s(this,R),s(this,x))}))}get_effect_pending(){return s(this,ee).call(this),be(s(this,R))}error(e){var r=s(this,T).onerror;let a=s(this,T).failed;if(!r&&!a)throw e;s(this,S)&&(ie(s(this,S)),n(this,S,null)),s(this,m)&&(ie(s(this,m)),n(this,m,null)),s(this,w)&&(ie(s(this,w)),n(this,w,null)),L&&(Z(s(this,H)),Ye(),Z(qe()));var f=!1,i=!1;const o=()=>{if(f){Ue();return}f=!0,i&&Ge(),s(this,w)!==null&&se(s(this,w),()=>{n(this,w,null)}),p(this,l,K).call(this,()=>{p(this,l,oe).call(this)})},d=c=>{try{i=!0,r==null||r(c,o),i=!1}catch(h){$(h,s(this,g)&&s(this,g).parent)}a&&n(this,w,p(this,l,K).call(this,()=>{try{return F(()=>{var h=P;h.b=this,h.f|=_e,a(s(this,E),()=>c,()=>o)})}catch(h){return $(h,s(this,g).parent),null}}))};j(()=>{var c;try{c=this.transform_error(e)}catch(h){$(h,s(this,g)&&s(this,g).parent);return}c!==null&&typeof c=="object"&&typeof c.then=="function"?c.then(d,h=>$(h,s(this,g)&&s(this,g).parent)):d(c)})}}E=new WeakMap,H=new WeakMap,T=new WeakMap,V=new WeakMap,g=new WeakMap,S=new WeakMap,m=new WeakMap,w=new WeakMap,A=new WeakMap,x=new WeakMap,N=new WeakMap,B=new WeakMap,Y=new WeakMap,q=new WeakMap,R=new WeakMap,ee=new WeakMap,l=new WeakSet,Ae=function(){try{n(this,S,F(()=>s(this,V).call(this,s(this,E))))}catch(e){this.error(e)}},Re=function(e){const r=s(this,T).failed;r&&n(this,w,F(()=>{r(s(this,E),()=>e,()=>()=>{})}))},ke=function(){const e=s(this,T).pending;e&&(this.is_pending=!0,n(this,m,F(()=>e(s(this,E)))),j(()=>{var r=n(this,A,document.createDocumentFragment()),a=me();r.append(a),n(this,S,p(this,l,K).call(this,()=>F(()=>s(this,V).call(this,a)))),s(this,N)===0&&(s(this,E).before(r),n(this,A,null),se(s(this,m),()=>{n(this,m,null)}),p(this,l,J).call(this,ge))}))},oe=function(){try{if(this.is_pending=this.has_pending_snippet(),n(this,N,0),n(this,x,0),n(this,S,F(()=>{s(this,V).call(this,s(this,E))})),s(this,N)>0){var e=n(this,A,document.createDocumentFragment());Pe(s(this,S),e);const r=s(this,T).pending;n(this,m,F(()=>r(s(this,E))))}else p(this,l,J).call(this,ge)}catch(r){this.error(r)}},J=function(e){this.is_pending=!1,e.transfer_effects(s(this,Y),s(this,q))},K=function(e){var r=P,a=we,f=Te;Q(s(this,g)),X(s(this,g)),ve(s(this,g).ctx);try{return We.ensure(),e()}catch(i){return je(i),null}finally{Q(r),X(a),ve(f)}},fe=function(e,r){var a;if(!this.has_pending_snippet()){this.parent&&p(a=this.parent,l,fe).call(a,e,r);return}n(this,N,s(this,N)+e),s(this,N)===0&&(p(this,l,J).call(this,r),s(this,m)&&se(s(this,m),()=>{n(this,m,null)}),s(this,A)&&(s(this,E).before(s(this,A)),n(this,A,null)))};function St(t){return t.endsWith("capture")&&t!=="gotpointercapture"&&t!=="lostpointercapture"}const _t=["beforeinput","click","change","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"];function At(t){return _t.includes(t)}const pt={formnovalidate:"formNoValidate",ismap:"isMap",nomodule:"noModule",playsinline:"playsInline",readonly:"readOnly",defaultvalue:"defaultValue",defaultchecked:"defaultChecked",srcobject:"srcObject",novalidate:"noValidate",allowfullscreen:"allowFullscreen",disablepictureinpicture:"disablePictureInPicture",disableremoteplayback:"disableRemotePlayback"};function Rt(t){return t=t.toLowerCase(),pt[t]??t}const gt=["touchstart","touchmove"];function vt(t){return gt.includes(t)}const yt=["textarea","script","style","title"];function kt(t){return yt.includes(t)}const M=Symbol("events"),Ne=new Set,le=new Set;function bt(t,e,r,a={}){function f(i){if(a.capture||ce.call(e,i),!i.cancelBubble)return Ke(()=>r==null?void 0:r.call(this,i))}return t.startsWith("pointer")||t.startsWith("touch")||t==="wheel"?j(()=>{e.addEventListener(t,f,a)}):e.addEventListener(t,f,a),f}function Nt(t,e,r,a,f){var i={capture:a,passive:f},o=bt(t,e,r,i);(e===document.body||e===window||e===document||e instanceof HTMLMediaElement)&&Qe(()=>{e.removeEventListener(t,o,i)})}function Dt(t,e,r){(e[M]??(e[M]={}))[t]=r}function Ot(t){for(var e=0;e{throw O});throw k}}finally{t[M]=e,delete t.currentTarget,X(C),Q(W)}}}function Ft(t,e){var r=e==null?"":typeof e=="object"?`${e}`:e;r!==(t.__t??(t.__t=t.nodeValue))&&(t.__t=r,t.nodeValue=`${r}`)}function Et(t,e){return De(t,e)}function It(t,e){ae(),e.intro=e.intro??!1;const r=e.target,a=L,f=I;try{for(var i=Xe(r);i&&(i.nodeType!==Se||i.data!==Ze);)i=et(i);if(!i)throw ne;G(!0),Z(i);const o=De(t,{...e,anchor:i});return G(!1),o}catch(o){if(o instanceof Error&&o.message.split(` +`).some(d=>d.startsWith("https://svelte.dev/e/")))throw o;return o!==ne&&console.warn("Failed to hydrate: ",o),e.recover===!1&&tt(),ae(),rt(r),G(!1),Et(t,e)}finally{G(a),Z(f)}}const U=new Map;function De(t,{target:e,anchor:r,props:a={},events:f,context:i,intro:o=!0,transformError:d}){ae();var c=void 0,h=st(()=>{var C=r??e.appendChild(me());ht(C,{pending:()=>{}},v=>{at({});var _=Te;if(i&&(_.c=i),f&&(a.$$events=f),L&<(v,null),c=t(v,a)||{},L&&(P.nodes.end=I,I===null||I.nodeType!==Se||I.data!==nt))throw ot(),ne;ft()},d);var W=new Set,k=v=>{for(var _=0;_{var D;for(var v of W)for(const b of[e,document]){var _=U.get(b),y=_.get(v);--y==0?(b.removeEventListener(v,ce),_.delete(v),_.size===0&&U.delete(b)):_.set(v,y)}le.delete(k),C!==r&&((D=C.parentNode)==null||D.removeChild(C))}});return ue.set(c,h),c}let ue=new WeakMap;function Lt(t,e){const r=ue.get(t);return r?(ue.delete(t),r(e)):Promise.resolve()}export{St as a,Ot as b,bt as c,Dt as d,At as e,Nt as f,It as h,kt as i,Et as m,Rt as n,Ft as s,Lt as u}; diff --git a/frontend/dist/_app/immutable/chunks/CiUbu-1F.js b/frontend/dist/_app/immutable/chunks/CiUbu-1F.js deleted file mode 100644 index fb6b69a..0000000 --- a/frontend/dist/_app/immutable/chunks/CiUbu-1F.js +++ /dev/null @@ -1 +0,0 @@ -import{b as p,E as t}from"./VJhVTb1A.js";import{B as c}from"./U5yjBaoU.js";function E(r,s,...a){var e=new c(r);p(()=>{const n=s()??null;e.ensure(n,n&&(o=>n(o,...a)))},t)}export{E as s}; diff --git a/frontend/dist/_app/immutable/chunks/Cz6m6K8p.js b/frontend/dist/_app/immutable/chunks/Cz6m6K8p.js deleted file mode 100644 index e41051c..0000000 --- a/frontend/dist/_app/immutable/chunks/Cz6m6K8p.js +++ /dev/null @@ -1 +0,0 @@ -import{j as u,i as t,w as l,k as o}from"./VJhVTb1A.js";function c(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function a(e){t===null&&c(),l&&t.l!==null?i(t).m.push(e):u(()=>{const n=o(e);if(typeof n=="function")return n})}function f(e){t===null&&c(),a(()=>()=>o(e))}function i(e){var n=e.l;return n.u??(n.u={a:[],b:[],m:[]})}export{f as a,a as o}; diff --git a/frontend/dist/_app/immutable/chunks/D2EIOFXK.js b/frontend/dist/_app/immutable/chunks/D2EIOFXK.js new file mode 100644 index 0000000..31edb79 --- /dev/null +++ b/frontend/dist/_app/immutable/chunks/D2EIOFXK.js @@ -0,0 +1 @@ +var yn=Object.defineProperty;var Rt=e=>{throw TypeError(e)};var En=(e,t,n)=>t in e?yn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var ie=(e,t,n)=>En(e,typeof t!="symbol"?t+"":t,n),tt=(e,t,n)=>t.has(e)||Rt("Cannot "+n);var u=(e,t,n)=>(tt(e,t,"read from private field"),n?n.call(e):t.get(e)),k=(e,t,n)=>t.has(e)?Rt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),ye=(e,t,n,r)=>(tt(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),I=(e,t,n)=>(tt(e,t,"access private method"),n);var gn=Array.isArray,mn=Array.prototype.indexOf,Te=Array.prototype.includes,yr=Array.from,Er=Object.defineProperty,Ie=Object.getOwnPropertyDescriptor,bn=Object.getOwnPropertyDescriptors,Tn=Object.prototype,An=Array.prototype,Lt=Object.getPrototypeOf,Nt=Object.isExtensible;function gr(e){return typeof e=="function"}const Sn=()=>{};function mr(e){return e()}function Rn(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function br(e,t,n=!1){return e===void 0?n?t():t:e}function Tr(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);const n=[];for(const r of e)if(n.push(r),n.length===t)break;return n}const A=2,Ae=4,Le=8,vt=1<<24,Z=16,G=32,_e=64,Nn=128,F=512,b=1024,x=2048,z=4096,H=8192,q=16384,we=32768,xt=1<<25,Ge=65536,st=1<<17,xn=1<<18,je=1<<19,Yt=1<<20,Ar=1<<25,ve=65536,it=1<<21,Je=1<<22,ee=1<<23,ue=Symbol("$state"),Sr=Symbol("legacy props"),Rr=Symbol(""),K=new class extends Error{constructor(){super(...arguments);ie(this,"name","StaleReactionError");ie(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Ft;const xr=!!((Ft=globalThis.document)!=null&&Ft.contentType)&&globalThis.document.contentType.includes("xml"),Or=1,Qe=3,Ht=8;function On(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function kn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function kr(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Dn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function In(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Mn(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Cn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Dr(){throw new Error("https://svelte.dev/e/hydration_failed")}function Ir(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Pn(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function Fn(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Ln(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Mr(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const Cr=1,Pr=2,Fr=4,Lr=8,jr=16,Yr=1,Hr=2,qr=4,Ur=8,Vr=16,Br=1,Gr=2,jn="[",Yn="[!",zr="[?",Hn="]",dt={},S=Symbol(),qn="http://www.w3.org/1999/xhtml",Kr="http://www.w3.org/2000/svg",Xr="http://www.w3.org/1998/Math/MathML",$r="@attach";function ht(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Zr(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Wr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let ne=!1;function Jr(e){ne=e}let T;function Se(e){if(e===null)throw ht(),dt;return T=e}function Qr(){return Se(se(T))}function es(e){if(ne){if(se(T)!==null)throw ht(),dt;T=e}}function ts(e=1){if(ne){for(var t=e,n=T;t--;)n=se(n);T=n}}function ns(e=!0){for(var t=0,n=T;;){if(n.nodeType===Ht){var r=n.data;if(r===Hn){if(t===0)return n;t-=1}else(r===jn||r===Yn||r[0]==="["&&!isNaN(Number(r.slice(1))))&&(t+=1)}var s=se(n);e&&n.remove(),n=s}}function rs(e){if(!e||e.nodeType!==Ht)throw ht(),dt;return e.data}function qt(e){return e===this.v}function Un(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Ut(e){return!Un(e,this.v)}let et=!1;function ss(){et=!0}let g=null;function ze(e){g=e}function is(e){return Vn().get(e)}function ls(e,t=!1,n){g={p:g,i:!1,c:null,e:null,s:e,x:null,r:y,l:et&&!t?{s:null,u:null,$:[]}:null}}function fs(e){var t=g,n=t.e;if(n!==null){t.e=null;for(var r of n)ln(r)}return t.i=!0,g=t.p,{}}function Ye(){return!et||g!==null&&g.l===null}function Vn(e){return g===null&&On(),g.c??(g.c=new Map(Bn(g)||void 0))}function Bn(e){let t=e.p;for(;t!==null;){const n=t.c;if(n!==null)return n;t=t.p}return null}let fe=[];function Vt(){var e=fe;fe=[],Rn(e)}function lt(e){if(fe.length===0&&!Me){var t=fe;queueMicrotask(()=>{t===fe&&Vt()})}fe.push(e)}function Gn(){for(;fe.length>0;)Vt()}function zn(e){var t=y;if(t===null)return p.f|=ee,e;if((t.f&we)===0&&(t.f&Ae)===0)throw e;Ke(e,t)}function Ke(e,t){for(;t!==null;){if((t.f&Nn)!==0){if((t.f&we)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Kn=-7169;function E(e,t){e.f=e.f&Kn|t}function pt(e){(e.f&F)!==0||e.deps===null?E(e,b):E(e,z)}function Bt(e){if(e!==null)for(const t of e)(t.f&A)===0||(t.f&ve)===0||(t.f^=ve,Bt(t.deps))}function Xn(e,t,n){(e.f&x)!==0?t.add(e):(e.f&z)!==0&&n.add(e),Bt(e.deps),E(e,b)}const W=new Set;let w=null,R=null,ft=null,Me=!1,nt=!1,Ee=null,qe=null;var Ot=0;let $n=1;var ge,me,X,V,Pe,C,Fe,Q,$,B,be,oe,m,Ue,Gt,Ve,at,ot,zt;const We=class We{constructor(){k(this,m);ie(this,"id",$n++);ie(this,"current",new Map);ie(this,"previous",new Map);k(this,ge,new Set);k(this,me,new Set);k(this,X,new Map);k(this,V,new Map);k(this,Pe,null);k(this,C,[]);k(this,Fe,[]);k(this,Q,new Set);k(this,$,new Set);k(this,B,new Map);ie(this,"is_fork",!1);k(this,be,!1);k(this,oe,new Set)}skip_effect(t){u(this,B).has(t)||u(this,B).set(t,{d:[],m:[]})}unskip_effect(t){var n=u(this,B).get(t);if(n){u(this,B).delete(t);for(var r of n.d)E(r,x),this.schedule(r);for(r of n.m)E(r,z),this.schedule(r)}}capture(t,n,r=!1){n!==S&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&ee)===0&&(this.current.set(t,[t.v,r]),R==null||R.set(t,t.v))}activate(){w=this}deactivate(){w=null,R=null}flush(){try{nt=!0,w=this,I(this,m,Ve).call(this)}finally{Ot=0,ft=null,Ee=null,qe=null,nt=!1,w=null,R=null,te.clear()}}discard(){for(const t of u(this,me))t(this);u(this,me).clear(),W.delete(this)}register_created_effect(t){u(this,Fe).push(t)}increment(t,n){let r=u(this,X).get(n)??0;if(u(this,X).set(n,r+1),t){let s=u(this,V).get(n)??0;u(this,V).set(n,s+1)}}decrement(t,n,r){let s=u(this,X).get(n)??0;if(s===1?u(this,X).delete(n):u(this,X).set(n,s-1),t){let i=u(this,V).get(n)??0;i===1?u(this,V).delete(n):u(this,V).set(n,i-1)}u(this,be)||r||(ye(this,be,!0),lt(()=>{ye(this,be,!1),this.flush()}))}transfer_effects(t,n){for(const r of t)u(this,Q).add(r);for(const r of n)u(this,$).add(r);t.clear(),n.clear()}oncommit(t){u(this,ge).add(t)}ondiscard(t){u(this,me).add(t)}settled(){return(u(this,Pe)??ye(this,Pe,jt())).promise}static ensure(){if(w===null){const t=w=new We;nt||(W.add(w),Me||lt(()=>{w===t&&t.flush()}))}return w}apply(){{R=null;return}}schedule(t){var s;if(ft=t,(s=t.b)!=null&&s.is_pending&&(t.f&(Ae|Le|vt))!==0&&(t.f&we)===0){t.b.defer_effect(t);return}for(var n=t;n.parent!==null;){n=n.parent;var r=n.f;if(Ee!==null&&n===y&&(p===null||(p.f&A)===0))return;if((r&(_e|G))!==0){if((r&b)===0)return;n.f^=b}}u(this,C).push(n)}};ge=new WeakMap,me=new WeakMap,X=new WeakMap,V=new WeakMap,Pe=new WeakMap,C=new WeakMap,Fe=new WeakMap,Q=new WeakMap,$=new WeakMap,B=new WeakMap,be=new WeakMap,oe=new WeakMap,m=new WeakSet,Ue=function(){return this.is_fork||u(this,V).size>0},Gt=function(){for(const r of u(this,oe))for(const s of u(r,V).keys()){for(var t=!1,n=s;n.parent!==null;){if(u(this,B).has(n)){t=!0;break}n=n.parent}if(!t)return!0}return!1},Ve=function(){var a,f;if(Ot++>1e3&&(W.delete(this),Wn()),!I(this,m,Ue).call(this)){for(const l of u(this,Q))u(this,$).delete(l),E(l,x),this.schedule(l);for(const l of u(this,$))E(l,z),this.schedule(l)}const t=u(this,C);ye(this,C,[]),this.apply();var n=Ee=[],r=[],s=qe=[];for(const l of t)try{I(this,m,at).call(this,l,n,r)}catch(c){throw $t(l),c}if(w=null,s.length>0){var i=We.ensure();for(const l of s)i.schedule(l)}if(Ee=null,qe=null,I(this,m,Ue).call(this)||I(this,m,Gt).call(this)){I(this,m,ot).call(this,r),I(this,m,ot).call(this,n);for(const[l,c]of u(this,B))Xt(l,c)}else{u(this,X).size===0&&W.delete(this),u(this,Q).clear(),u(this,$).clear();for(const l of u(this,ge))l(this);u(this,ge).clear(),kt(r),kt(n),(a=u(this,Pe))==null||a.resolve()}var o=w;if(u(this,C).length>0){const l=o??(o=this);u(l,C).push(...u(this,C).filter(c=>!u(l,C).includes(c)))}o!==null&&(W.add(o),I(f=o,m,Ve).call(f)),W.has(this)||I(this,m,zt).call(this)},at=function(t,n,r){t.f^=b;for(var s=t.first;s!==null;){var i=s.f,o=(i&(G|_e))!==0,a=o&&(i&b)!==0,f=a||(i&H)!==0||u(this,B).has(s);if(!f&&s.fn!==null){o?s.f^=b:(i&Ae)!==0?n.push(s):He(s)&&((i&Z)!==0&&u(this,$).add(s),Ne(s));var l=s.first;if(l!==null){s=l;continue}}for(;s!==null;){var c=s.next;if(c!==null){s=c;break}s=s.parent}}},ot=function(t){for(var n=0;n!this.current.has(_));if(s.length===0)t&&d.discard();else if(n.length>0){d.activate();var i=new Set,o=new Map;for(var a of n)Kt(a,s,i,o);o=new Map;var f=[...d.current.keys()].filter(_=>this.current.has(_)?this.current.get(_)[0]!==_:!0);for(const _ of u(this,Fe))(_.f&(q|H|st))===0&&wt(_,f,o)&&((_.f&(Je|Z))!==0?(E(_,x),d.schedule(_)):u(d,Q).add(_));if(u(d,C).length>0){d.apply();for(var l of u(d,C))I(c=d,m,at).call(c,l,[],[]);ye(d,C,[])}d.deactivate()}}for(const d of W)u(d,oe).has(this)&&(u(d,oe).delete(this),u(d,oe).size===0&&!I(h=d,m,Ue).call(h)&&(d.activate(),I(v=d,m,Ve).call(v)))};let de=We;function Zn(e){var t=Me;Me=!0;try{for(var n;;){if(Gn(),w===null)return n;w.flush()}}finally{Me=t}}function Wn(){try{Cn()}catch(e){Ke(e,ft)}}let j=null;function kt(e){var t=e.length;if(t!==0){for(var n=0;n0)){te.clear();for(const s of j){if((s.f&(q|H))!==0)continue;const i=[s];let o=s.parent;for(;o!==null;)j.has(o)&&(j.delete(o),i.push(o)),o=o.parent;for(let a=i.length-1;a>=0;a--){const f=i[a];(f.f&(q|H))===0&&Ne(f)}}j.clear()}}j=null}}function Kt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const s of e.reactions){const i=s.f;(i&A)!==0?Kt(s,t,n,r):(i&(Je|Z))!==0&&(i&x)===0&&wt(s,t,r)&&(E(s,x),yt(s))}}function wt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const s of e.deps){if(Te.call(t,s))return!0;if((s.f&A)!==0&&wt(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function yt(e){w.schedule(e)}function Xt(e,t){if(!((e.f&G)!==0&&(e.f&b)!==0)){(e.f&x)!==0?t.d.push(e):(e.f&z)!==0&&t.m.push(e),E(e,b);for(var n=e.first;n!==null;)Xt(n,t),n=n.next}}function $t(e){E(e,b);for(var t=e.first;t!==null;)$t(t),t=t.next}function Jn(e,t,n,r){const s=Ye()?Et:tr;var i=e.filter(v=>!v.settled);if(n.length===0&&i.length===0){r(t.map(s));return}var o=y,a=Qn(),f=i.length===1?i[0].promise:i.length>1?Promise.all(i.map(v=>v.promise)):null;function l(v){a();try{r(v)}catch(d){(o.f&q)===0&&Ke(d,o)}Xe()}if(n.length===0){f.then(()=>l(t.map(s)));return}var c=Zt();function h(){Promise.all(n.map(v=>er(v))).then(v=>l([...t.map(s),...v])).catch(v=>Ke(v,o)).finally(()=>c())}f?f.then(()=>{a(),h(),Xe()}):h()}function Qn(){var e=y,t=p,n=g,r=w;return function(i=!0){Re(e),re(t),ze(n),i&&(e.f&q)===0&&(r==null||r.activate(),r==null||r.apply())}}function Xe(e=!0){Re(null),re(null),ze(null),e&&(w==null||w.deactivate())}function Zt(){var e=y,t=e.b,n=w,r=t.is_rendered();return t.update_pending_count(1,n),n.increment(r,e),(s=!1)=>{t.update_pending_count(-1,n),n.decrement(r,e,s)}}function Et(e){var t=A|x,n=p!==null&&(p.f&A)!==0?p:null;return y!==null&&(y.f|=je),{ctx:g,deps:null,effects:null,equals:qt,f:t,fn:e,reactions:null,rv:0,v:S,wv:0,parent:n??y,ac:null}}function er(e,t,n){let r=y;r===null&&kn();var s=void 0,i=mt(S),o=!p,a=new Map;return cr(()=>{var d;var f=y,l=jt();s=l.promise;try{Promise.resolve(e()).then(l.resolve,l.reject).finally(Xe)}catch(_){l.reject(_),Xe()}var c=w;if(o){if((f.f&we)!==0)var h=Zt();if(r.b.is_rendered())(d=a.get(c))==null||d.reject(K),a.delete(c);else{for(const _ of a.values())_.reject(K);a.clear()}a.set(c,l)}const v=(_,O=void 0)=>{if(h){var N=O===K;h(N)}if(!(O===K||(f.f&q)!==0)){if(c.activate(),O)i.f|=ee,ct(i,O);else{(i.f&ee)!==0&&(i.f^=ee),ct(i,_);for(const[xe,Oe]of a){if(a.delete(xe),xe===c)break;Oe.reject(K)}}c.deactivate()}};l.promise.then(v,_=>v(null,_||"unknown"))}),ur(()=>{for(const f of a.values())f.reject(K)}),new Promise(f=>{function l(c){function h(){c===s?f(i):l(s)}c.then(h,h)}l(s)})}function as(e){const t=Et(e);return cn(t),t}function tr(e){const t=Et(e);return t.equals=Ut,t}function nr(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!Qt&&ir()}return t}function ir(){Qt=!1;for(const e of ut)(e.f&b)!==0&&E(e,z),He(e)&&Ne(e);ut.clear()}function rt(e){le(e,e.v+1)}function en(e,t,n){var r=e.reactions;if(r!==null)for(var s=Ye(),i=r.length,o=0;o{if(ce===i)return a();var f=p,l=ce;re(null),Pt(i);var c=a();return re(f),Pt(l),c};return r&&n.set("length",J(e.length)),new Proxy(e,{defineProperty(a,f,l){(!("value"in l)||l.configurable===!1||l.enumerable===!1||l.writable===!1)&&Pn();var c=n.get(f);return c===void 0?o(()=>{var h=J(l.value);return n.set(f,h),h}):le(c,l.value,!0),!0},deleteProperty(a,f){var l=n.get(f);if(l===void 0){if(f in a){const c=o(()=>J(S));n.set(f,c),rt(s)}}else le(l,S),rt(s);return!0},get(a,f,l){var d;if(f===ue)return e;var c=n.get(f),h=f in a;if(c===void 0&&(!h||(d=Ie(a,f))!=null&&d.writable)&&(c=o(()=>{var _=ke(h?a[f]:S),O=J(_);return O}),n.set(f,c)),c!==void 0){var v=De(c);return v===S?void 0:v}return Reflect.get(a,f,l)},getOwnPropertyDescriptor(a,f){var l=Reflect.getOwnPropertyDescriptor(a,f);if(l&&"value"in l){var c=n.get(f);c&&(l.value=De(c))}else if(l===void 0){var h=n.get(f),v=h==null?void 0:h.v;if(h!==void 0&&v!==S)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return l},has(a,f){var v;if(f===ue)return!0;var l=n.get(f),c=l!==void 0&&l.v!==S||Reflect.has(a,f);if(l!==void 0||y!==null&&(!c||(v=Ie(a,f))!=null&&v.writable)){l===void 0&&(l=o(()=>{var d=c?ke(a[f]):S,_=J(d);return _}),n.set(f,l));var h=De(l);if(h===S)return!1}return c},set(a,f,l,c){var St;var h=n.get(f),v=f in a;if(r&&f==="length")for(var d=l;dJ(S)),n.set(d+"",_))}if(h===void 0)(!v||(St=Ie(a,f))!=null&&St.writable)&&(h=o(()=>J(void 0)),le(h,ke(l)),n.set(f,h));else{v=h.v!==S;var O=o(()=>ke(l));le(h,O)}var N=Reflect.getOwnPropertyDescriptor(a,f);if(N!=null&&N.set&&N.set.call(c,l),!v){if(r&&typeof f=="string"){var xe=n.get("length"),Oe=Number(f);Number.isInteger(Oe)&&Oe>=xe.v&&le(xe,Oe+1)}rt(s)}return!0},ownKeys(a){De(s);var f=Reflect.ownKeys(a).filter(h=>{var v=n.get(h);return v===void 0||v.v!==S});for(var[l,c]of n)c.v!==S&&!(l in a)&&f.push(l);return f},setPrototypeOf(){Fn()}})}function Dt(e){try{if(e!==null&&typeof e=="object"&&ue in e)return e[ue]}catch{}return e}function us(e,t){return Object.is(Dt(e),Dt(t))}var It,lr,tn,nn;function cs(){if(It===void 0){It=window,lr=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;tn=Ie(t,"firstChild").get,nn=Ie(t,"nextSibling").get,Nt(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),Nt(n)&&(n.__t=void 0)}}function $e(e=""){return document.createTextNode(e)}function Ze(e){return tn.call(e)}function se(e){return nn.call(e)}function _s(e,t){if(!ne)return Ze(e);var n=Ze(T);if(n===null)n=T.appendChild($e());else if(t&&n.nodeType!==Qe){var r=$e();return n==null||n.before(r),Se(r),r}return t&&bt(n),Se(n),n}function vs(e,t=!1){if(!ne){var n=Ze(e);return n instanceof Comment&&n.data===""?se(n):n}if(t){if((T==null?void 0:T.nodeType)!==Qe){var r=$e();return T==null||T.before(r),Se(r),r}bt(T)}return T}function ds(e,t=1,n=!1){let r=ne?T:e;for(var s;t--;)s=r,r=se(r);if(!ne)return r;if(n){if((r==null?void 0:r.nodeType)!==Qe){var i=$e();return r===null?s==null||s.after(i):r.before(i),Se(i),i}bt(r)}return Se(r),r}function fr(e){e.textContent=""}function hs(){return!1}function ps(e,t,n){return document.createElementNS(t??qn,e,void 0)}function bt(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===Qe;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function ws(e,t){if(t){const n=document.body;e.autofocus=!0,lt(()=>{document.activeElement===n&&e.focus()})}}function ys(e){ne&&Ze(e)!==null&&fr(e)}let Mt=!1;function ar(){Mt||(Mt=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function Tt(e){var t=p,n=y;re(null),Re(null);try{return e()}finally{re(t),Re(n)}}function Es(e,t,n,r=n){e.addEventListener(t,()=>Tt(n));const s=e.__on_r;s?e.__on_r=()=>{s(),r(!0)}:e.__on_r=()=>r(!0),ar()}function rn(e){y===null&&(p===null&&Mn(),In()),pe&&Dn()}function or(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function U(e,t){var n=y;n!==null&&(n.f&H)!==0&&(e|=H);var r={ctx:g,deps:null,nodes:null,f:e|x|F,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};w==null||w.register_created_effect(r);var s=r;if((e&Ae)!==0)Ee!==null?Ee.push(r):de.ensure().schedule(r);else if(t!==null){try{Ne(r)}catch(o){throw he(r),o}s.deps===null&&s.teardown===null&&s.nodes===null&&s.first===s.last&&(s.f&je)===0&&(s=s.first,(e&Z)!==0&&(e&Ge)!==0&&s!==null&&(s.f|=Ge))}if(s!==null&&(s.parent=n,n!==null&&or(s,n),p!==null&&(p.f&A)!==0&&(e&_e)===0)){var i=p;(i.effects??(i.effects=[])).push(s)}return r}function sn(){return p!==null&&!Y}function ur(e){const t=U(Le,null);return E(t,b),t.teardown=e,t}function gs(e){rn();var t=y.f,n=!p&&(t&G)!==0&&(t&we)===0;if(n){var r=g;(r.e??(r.e=[])).push(e)}else return ln(e)}function ln(e){return U(Ae|Yt,e)}function ms(e){return rn(),U(Le|Yt,e)}function bs(e){de.ensure();const t=U(_e|je,e);return(n={})=>new Promise(r=>{n.outro?dr(t,()=>{he(t),r(void 0)}):(he(t),r(void 0))})}function Ts(e){return U(Ae,e)}function cr(e){return U(Je|je,e)}function As(e,t=0){return U(Le|t,e)}function Ss(e,t=[],n=[],r=[]){Jn(r,t,n,s=>{U(Le,()=>e(...s.map(De)))})}function Rs(e,t=0){var n=U(Z|t,e);return n}function Ns(e,t=0){var n=U(vt|t,e);return n}function xs(e){return U(G|je,e)}function fn(e){var t=e.teardown;if(t!==null){const n=pe,r=p;Ct(!0),re(null);try{t.call(null)}finally{Ct(n),re(r)}}}function At(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&Tt(()=>{s.abort(K)});var r=n.next;(n.f&_e)!==0?n.parent=null:he(n,t),n=r}}function _r(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&G)===0&&he(t),t=n}}function he(e,t=!0){var n=!1;(t||(e.f&xn)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(vr(e.nodes.start,e.nodes.end),n=!0),E(e,xt),At(e,t&&!n),Ce(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const i of r)i.stop();fn(e),e.f^=xt,e.f|=q;var s=e.parent;s!==null&&s.first!==null&&an(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function vr(e,t){for(;e!==null;){var n=e===t?null:se(e);e.remove(),e=n}}function an(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function dr(e,t,n=!0){var r=[];on(e,r,!0);var s=()=>{n&&he(e),t&&t()},i=r.length;if(i>0){var o=()=>--i||s();for(var a of r)a.out(o)}else s()}function on(e,t,n){if((e.f&H)===0){e.f^=H;var r=e.nodes&&e.nodes.t;if(r!==null)for(const a of r)(a.is_global||n)&&t.push(a);for(var s=e.first;s!==null;){var i=s.next,o=(s.f&Ge)!==0||(s.f&G)!==0&&(e.f&Z)!==0;on(s,t,o?n:!1),s=i}}}function Os(e){un(e,!0)}function un(e,t){if((e.f&H)!==0){e.f^=H,(e.f&b)===0&&(E(e,x),de.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&Ge)!==0||(n.f&G)!==0;un(n,s?t:!1),n=r}var i=e.nodes&&e.nodes.t;if(i!==null)for(const o of i)(o.is_global||t)&&o.in()}}function ks(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:se(n);t.append(n),n=s}}let Be=!1,pe=!1;function Ct(e){pe=e}let p=null,Y=!1;function re(e){p=e}let y=null;function Re(e){y=e}let L=null;function cn(e){p!==null&&(L===null?L=[e]:L.push(e))}let D=null,M=0,P=null;function hr(e){P=e}let _n=1,ae=0,ce=ae;function Pt(e){ce=e}function vn(){return++_n}function He(e){var t=e.f;if((t&x)!==0)return!0;if(t&A&&(e.f&=~ve),(t&z)!==0){for(var n=e.deps,r=n.length,s=0;se.wv)return!0}(t&F)!==0&&R===null&&E(e,b)}return!1}function dn(e,t,n=!0){var r=e.reactions;if(r!==null&&!(L!==null&&Te.call(L,e)))for(var s=0;s{e.ac.abort(K)}),e.ac=null);try{e.f|=it;var c=e.fn,h=c();e.f|=we;var v=e.deps,d=w==null?void 0:w.is_fork;if(D!==null){var _;if(d||Ce(e,M),v!==null&&M>0)for(v.length=M+D.length,_=0;_{const n=s()??null;e.ensure(n,n&&(o=>n(o,...a)))},t)}export{E as s}; diff --git a/frontend/dist/_app/immutable/chunks/DmjnTBOd.js b/frontend/dist/_app/immutable/chunks/DmjnTBOd.js deleted file mode 100644 index e7c9d0c..0000000 --- a/frontend/dist/_app/immutable/chunks/DmjnTBOd.js +++ /dev/null @@ -1 +0,0 @@ -import{i as d,u as g,j as c,k as m,l as i,n as b,o as p,q as v,t as k,v as h}from"./VJhVTb1A.js";function x(n=!1){const s=d,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(n){let o=0,t={};const _=k(()=>{let l=!1;const r=s.s;for(const a in r)r[a]!==t[a]&&(t[a]=r[a],l=!0);return l&&o++,o});f=()=>p(_)}e.b.length&&g(()=>{u(s,f),i(e.b)}),c(()=>{const o=m(()=>e.m.map(b));return()=>{for(const t of o)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}h();export{x as i}; diff --git a/frontend/dist/_app/immutable/chunks/Dy37i2LO.js b/frontend/dist/_app/immutable/chunks/Dy37i2LO.js new file mode 100644 index 0000000..23cf7a2 --- /dev/null +++ b/frontend/dist/_app/immutable/chunks/Dy37i2LO.js @@ -0,0 +1 @@ +import{b as A,h as P,w as x,E as D,O as N,P as Y,y as B,Q as S,f as L,S as C,g as F,U as Z,V as q,j,o as m,W as z,X as O,Y as y,Z as H,_ as Q,D as w,$ as V,a0 as W,a1 as X,a2 as J,a3 as k,i as rr,a4 as er,a5 as nr,G as tr,a6 as sr,a7 as ar,a8 as v,a9 as M}from"./D2EIOFXK.js";import{B as U}from"./lwvwhigX.js";let b=!1;function ir(r){var e=b;try{return b=!1,[r(),b]}finally{b=e}}function cr(r,e,n=!1){var t;P&&(t=L,x());var s=new U(r),i=n?D:0;function o(a,f){if(P){var c=N(t);if(a!==parseInt(c.substring(1))){var u=Y();B(u),s.anchor=u,S(!1),s.ensure(a,f),S(!0);return}}s.ensure(a,f)}A(()=>{var a=!1;e((f,c=0)=>{a=!0,o(c,f)}),a||o(-1,null)},i)}function dr(r,e,n){var t;P&&(t=L,x());var s=new U(r);A(()=>{var i=e()??null;if(P){var o=N(t),a=o===C,f=i!==null;if(a!==f){var c=Y();B(c),s.anchor=c,S(!1),s.ensure(i,i&&(u=>n(u,i))),S(!0);return}}s.ensure(i,i&&(u=>n(u,i)))},D)}function T(r,e){return r===e||(r==null?void 0:r[O])===e}function pr(r={},e,n,t){var s=F.r,i=m;return Z(()=>{var o,a;return q(()=>{o=a,a=[],j(()=>{r!==n(...a)&&(e(r,...a),o&&T(n(...o),r)&&e(null,...o))})}),()=>{let f=i;for(;f!==s&&f.parent!==null&&f.parent.f&z;)f=f.parent;const c=()=>{a&&T(n(...a),r)&&e(null,...a)},u=f.teardown;f.teardown=()=>{c(),u==null||u()}}}),r}const fr={get(r,e){if(!r.exclude.includes(e))return r.props[e]},set(r,e){return!1},getOwnPropertyDescriptor(r,e){if(!r.exclude.includes(e)&&e in r.props)return{enumerable:!0,configurable:!0,value:r.props[e]}},has(r,e){return r.exclude.includes(e)?!1:e in r.props},ownKeys(r){return Reflect.ownKeys(r.props).filter(e=>!r.exclude.includes(e))}};function _r(r,e,n){return new Proxy({props:r,exclude:e},fr)}const ur={get(r,e){let n=r.props.length;for(;n--;){let t=r.props[n];if(v(t)&&(t=t()),typeof t=="object"&&t!==null&&e in t)return t[e]}},set(r,e,n){let t=r.props.length;for(;t--;){let s=r.props[t];v(s)&&(s=s());const i=y(s,e);if(i&&i.set)return i.set(n),!0}return!1},getOwnPropertyDescriptor(r,e){let n=r.props.length;for(;n--;){let t=r.props[n];if(v(t)&&(t=t()),typeof t=="object"&&t!==null&&e in t){const s=y(t,e);return s&&!s.configurable&&(s.configurable=!0),s}}},has(r,e){if(e===O||e===M)return!1;for(let n of r.props)if(v(n)&&(n=n()),n!=null&&e in n)return!0;return!1},ownKeys(r){const e=[];for(let n of r.props)if(v(n)&&(n=n()),!!n){for(const t in n)e.includes(t)||e.push(t);for(const t of Object.getOwnPropertySymbols(n))e.includes(t)||e.push(t)}return e}};function vr(...r){return new Proxy({props:r},ur)}function hr(r,e,n,t){var E;var s=!rr||(n&er)!==0,i=(n&k)!==0,o=(n&ar)!==0,a=t,f=!0,c=()=>(f&&(f=!1,a=o?j(t):t),a);let u;if(i){var G=O in r||M in r;u=((E=y(r,e))==null?void 0:E.set)??(G&&e in r?l=>r[e]=l:void 0)}var p,R=!1;i?[p,R]=ir(()=>r[e]):p=r[e],p===void 0&&t!==void 0&&(p=c(),u&&(s&&H(),u(p)));var d;if(s?d=()=>{var l=r[e];return l===void 0?c():(f=!0,l)}:d=()=>{var l=r[e];return l!==void 0&&(a=void 0),l===void 0?a:l},s&&(n&Q)===0)return d;if(u){var K=r.$$legacy;return(function(l,h){return arguments.length>0?((!s||!h||K||R)&&u(h?d():l),l):d()})}var g=!1,_=((n&nr)!==0?tr:sr)(()=>(g=!1,d()));i&&w(_);var $=m;return(function(l,h){if(arguments.length>0){const I=h?w(_):s&&i?V(l):l;return W(_,I),g=!0,a!==void 0&&(a=I),l}return X&&g||($.f&J)!==0?_.v:w(_)})}export{pr as b,dr as c,cr as i,hr as p,_r as r,vr as s}; diff --git a/frontend/dist/_app/immutable/chunks/C14P8jaG.js b/frontend/dist/_app/immutable/chunks/IT17IssP.js similarity index 61% rename from frontend/dist/_app/immutable/chunks/C14P8jaG.js rename to frontend/dist/_app/immutable/chunks/IT17IssP.js index 04daf39..8b7225b 100644 --- a/frontend/dist/_app/immutable/chunks/C14P8jaG.js +++ b/frontend/dist/_app/immutable/chunks/IT17IssP.js @@ -1 +1 @@ -import{D as g,c as v,F as h,G as f,H as E,T as w,I as N,h as d,f as s,R as y,J as M,K as L,L as x,M as A}from"./VJhVTb1A.js";var m;const c=((m=globalThis==null?void 0:globalThis.window)==null?void 0:m.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function R(e){return(c==null?void 0:c.createHTML(e))??e}function p(e){var t=g("template");return t.innerHTML=R(e.replaceAll("","")),t.content}function n(e,t){var r=h;r.nodes===null&&(r.nodes={start:e,end:t,a:null,t:null})}function P(e,t){var r=(t&w)!==0,l=(t&N)!==0,a,u=!e.startsWith("");return()=>{if(d)return n(s,null),s;a===void 0&&(a=p(u?e:""+e),r||(a=f(a)));var o=l||E?document.importNode(a,!0):a.cloneNode(!0);if(r){var _=f(o),i=o.lastChild;n(_,i)}else n(o,o);return o}}function b(e,t,r="svg"){var l=!e.startsWith(""),a=`<${r}>${l?e:""+e}`,u;return()=>{if(d)return n(s,null),s;if(!u){var o=p(a),_=f(o);u=f(_)}var i=u.cloneNode(!0);return n(i,i),i}}function C(e,t){return b(e,t,"svg")}function D(e=""){if(!d){var t=v(e+"");return n(t,t),t}var r=s;return r.nodeType!==L?(r.before(r=v()),x(r)):A(r),n(r,r),r}function H(){if(d)return n(s,null),s;var e=document.createDocumentFragment(),t=document.createComment(""),r=v();return e.append(t,r),n(t,r),e}function F(e,t){if(d){var r=h;((r.f&y)===0||r.nodes.end===null)&&(r.nodes.end=s),M();return}e!==null&&e.before(t)}const I="5";var T;typeof window<"u"&&((T=window.__svelte??(window.__svelte={})).v??(T.v=new Set)).add(I);export{F as a,n as b,H as c,C as d,P as f,D as t}; +import{n as g,c as v,o as h,q as f,t as w,T as E,v as y,h as d,f as s,R as N,w as x,x as M,y as A,z as L}from"./D2EIOFXK.js";var m;const c=((m=globalThis==null?void 0:globalThis.window)==null?void 0:m.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function R(e){return(c==null?void 0:c.createHTML(e))??e}function p(e){var t=g("template");return t.innerHTML=R(e.replaceAll("","")),t.content}function n(e,t){var r=h;r.nodes===null&&(r.nodes={start:e,end:t,a:null,t:null})}function C(e,t){var r=(t&E)!==0,l=(t&y)!==0,a,u=!e.startsWith("");return()=>{if(d)return n(s,null),s;a===void 0&&(a=p(u?e:""+e),r||(a=f(a)));var o=l||w?document.importNode(a,!0):a.cloneNode(!0);if(r){var _=f(o),i=o.lastChild;n(_,i)}else n(o,o);return o}}function b(e,t,r="svg"){var l=!e.startsWith(""),a=`<${r}>${l?e:""+e}`,u;return()=>{if(d)return n(s,null),s;if(!u){var o=p(a),_=f(o);u=f(_)}var i=u.cloneNode(!0);return n(i,i),i}}function I(e,t){return b(e,t,"svg")}function D(e=""){if(!d){var t=v(e+"");return n(t,t),t}var r=s;return r.nodeType!==M?(r.before(r=v()),A(r)):L(r),n(r,r),r}function H(){if(d)return n(s,null),s;var e=document.createDocumentFragment(),t=document.createComment(""),r=v();return e.append(t,r),n(t,r),e}function S(e,t){if(d){var r=h;((r.f&N)===0||r.nodes.end===null)&&(r.nodes.end=s),x();return}e!==null&&e.before(t)}const O="5";var T;typeof window<"u"&&((T=window.__svelte??(window.__svelte={})).v??(T.v=new Set)).add(O);export{S as a,n as b,H as c,I as d,C as f,D as t}; diff --git a/frontend/dist/_app/immutable/chunks/VJhVTb1A.js b/frontend/dist/_app/immutable/chunks/VJhVTb1A.js deleted file mode 100644 index 7639c39..0000000 --- a/frontend/dist/_app/immutable/chunks/VJhVTb1A.js +++ /dev/null @@ -1 +0,0 @@ -var yn=Object.defineProperty;var Rt=e=>{throw TypeError(e)};var En=(e,t,n)=>t in e?yn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var le=(e,t,n)=>En(e,typeof t!="symbol"?t+"":t,n),tt=(e,t,n)=>t.has(e)||Rt("Cannot "+n);var u=(e,t,n)=>(tt(e,t,"read from private field"),n?n.call(e):t.get(e)),k=(e,t,n)=>t.has(e)?Rt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),ye=(e,t,n,r)=>(tt(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),I=(e,t,n)=>(tt(e,t,"access private method"),n);var gn=Array.isArray,mn=Array.prototype.indexOf,Ae=Array.prototype.includes,hr=Array.from,pr=Object.defineProperty,Ie=Object.getOwnPropertyDescriptor,bn=Object.getOwnPropertyDescriptors,Tn=Object.prototype,An=Array.prototype,Lt=Object.getPrototypeOf,Nt=Object.isExtensible;function wr(e){return typeof e=="function"}const Sn=()=>{};function yr(e){return e()}function Rn(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function Er(e,t){if(Array.isArray(e))return e;if(!(Symbol.iterator in e))return Array.from(e);const n=[];for(const r of e)if(n.push(r),n.length===t)break;return n}const T=2,Se=4,Le=8,vt=1<<24,Z=16,G=32,_e=64,Nn=128,F=512,m=1024,N=2048,z=4096,H=8192,q=16384,we=32768,Ot=1<<25,Ge=65536,st=1<<17,On=1<<18,je=1<<19,Yt=1<<20,gr=1<<25,ve=65536,it=1<<21,Je=1<<22,te=1<<23,ue=Symbol("$state"),mr=Symbol("legacy props"),br=Symbol(""),K=new class extends Error{constructor(){super(...arguments);le(this,"name","StaleReactionError");le(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Ft;const Ar=!!((Ft=globalThis.document)!=null&&Ft.contentType)&&globalThis.document.contentType.includes("xml"),Sr=1,Qe=3,Ht=8;function xn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Rr(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function kn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Dn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function In(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Mn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Nr(){throw new Error("https://svelte.dev/e/hydration_failed")}function Or(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Pn(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function Cn(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Fn(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function xr(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const kr=1,Dr=2,Ir=4,Mr=8,Pr=16,Cr=1,Fr=2,Lr=4,jr=8,Yr=16,Hr=1,qr=2,Ln="[",jn="[!",Ur="[?",Yn="]",dt={},A=Symbol(),Hn="http://www.w3.org/1999/xhtml",Vr="http://www.w3.org/2000/svg",Br="http://www.w3.org/1998/Math/MathML",Gr="@attach";function ht(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function zr(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Kr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let re=!1;function Xr(e){re=e}let b;function Re(e){if(e===null)throw ht(),dt;return b=e}function $r(){return Re(ie(b))}function Zr(e){if(re){if(ie(b)!==null)throw ht(),dt;b=e}}function Wr(e=1){if(re){for(var t=e,n=b;t--;)n=ie(n);b=n}}function Jr(e=!0){for(var t=0,n=b;;){if(n.nodeType===Ht){var r=n.data;if(r===Yn){if(t===0)return n;t-=1}else(r===Ln||r===jn||r[0]==="["&&!isNaN(Number(r.slice(1))))&&(t+=1)}var s=ie(n);e&&n.remove(),n=s}}function Qr(e){if(!e||e.nodeType!==Ht)throw ht(),dt;return e.data}function qt(e){return e===this.v}function qn(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Ut(e){return!qn(e,this.v)}let et=!1;function es(){et=!0}let O=null;function ze(e){O=e}function ts(e,t=!1,n){O={p:O,i:!1,c:null,e:null,s:e,x:null,r:y,l:et&&!t?{s:null,u:null,$:[]}:null}}function ns(e){var t=O,n=t.e;if(n!==null){t.e=null;for(var r of n)ln(r)}return t.i=!0,O=t.p,{}}function Ye(){return!et||O!==null&&O.l===null}let fe=[];function Vt(){var e=fe;fe=[],Rn(e)}function lt(e){if(fe.length===0&&!Me){var t=fe;queueMicrotask(()=>{t===fe&&Vt()})}fe.push(e)}function Un(){for(;fe.length>0;)Vt()}function Vn(e){var t=y;if(t===null)return p.f|=te,e;if((t.f&we)===0&&(t.f&Se)===0)throw e;Ke(e,t)}function Ke(e,t){for(;t!==null;){if((t.f&Nn)!==0){if((t.f&we)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Bn=-7169;function E(e,t){e.f=e.f&Bn|t}function pt(e){(e.f&F)!==0||e.deps===null?E(e,m):E(e,z)}function Bt(e){if(e!==null)for(const t of e)(t.f&T)===0||(t.f&ve)===0||(t.f^=ve,Bt(t.deps))}function Gn(e,t,n){(e.f&N)!==0?t.add(e):(e.f&z)!==0&&n.add(e),Bt(e.deps),E(e,m)}const W=new Set;let w=null,S=null,ft=null,Me=!1,nt=!1,Ee=null,qe=null;var xt=0;let zn=1;var me,be,X,V,Ce,P,Fe,ee,$,B,Te,oe,g,Ue,Gt,Ve,at,ot,zt;const We=class We{constructor(){k(this,g);le(this,"id",zn++);le(this,"current",new Map);le(this,"previous",new Map);k(this,me,new Set);k(this,be,new Set);k(this,X,new Map);k(this,V,new Map);k(this,Ce,null);k(this,P,[]);k(this,Fe,[]);k(this,ee,new Set);k(this,$,new Set);k(this,B,new Map);le(this,"is_fork",!1);k(this,Te,!1);k(this,oe,new Set)}skip_effect(t){u(this,B).has(t)||u(this,B).set(t,{d:[],m:[]})}unskip_effect(t){var n=u(this,B).get(t);if(n){u(this,B).delete(t);for(var r of n.d)E(r,N),this.schedule(r);for(r of n.m)E(r,z),this.schedule(r)}}capture(t,n,r=!1){n!==A&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&te)===0&&(this.current.set(t,[t.v,r]),S==null||S.set(t,t.v))}activate(){w=this}deactivate(){w=null,S=null}flush(){try{nt=!0,w=this,I(this,g,Ve).call(this)}finally{xt=0,ft=null,Ee=null,qe=null,nt=!1,w=null,S=null,ne.clear()}}discard(){for(const t of u(this,be))t(this);u(this,be).clear(),W.delete(this)}register_created_effect(t){u(this,Fe).push(t)}increment(t,n){let r=u(this,X).get(n)??0;if(u(this,X).set(n,r+1),t){let s=u(this,V).get(n)??0;u(this,V).set(n,s+1)}}decrement(t,n,r){let s=u(this,X).get(n)??0;if(s===1?u(this,X).delete(n):u(this,X).set(n,s-1),t){let i=u(this,V).get(n)??0;i===1?u(this,V).delete(n):u(this,V).set(n,i-1)}u(this,Te)||r||(ye(this,Te,!0),lt(()=>{ye(this,Te,!1),this.flush()}))}transfer_effects(t,n){for(const r of t)u(this,ee).add(r);for(const r of n)u(this,$).add(r);t.clear(),n.clear()}oncommit(t){u(this,me).add(t)}ondiscard(t){u(this,be).add(t)}settled(){return(u(this,Ce)??ye(this,Ce,jt())).promise}static ensure(){if(w===null){const t=w=new We;nt||(W.add(w),Me||lt(()=>{w===t&&t.flush()}))}return w}apply(){{S=null;return}}schedule(t){var s;if(ft=t,(s=t.b)!=null&&s.is_pending&&(t.f&(Se|Le|vt))!==0&&(t.f&we)===0){t.b.defer_effect(t);return}for(var n=t;n.parent!==null;){n=n.parent;var r=n.f;if(Ee!==null&&n===y&&(p===null||(p.f&T)===0))return;if((r&(_e|G))!==0){if((r&m)===0)return;n.f^=m}}u(this,P).push(n)}};me=new WeakMap,be=new WeakMap,X=new WeakMap,V=new WeakMap,Ce=new WeakMap,P=new WeakMap,Fe=new WeakMap,ee=new WeakMap,$=new WeakMap,B=new WeakMap,Te=new WeakMap,oe=new WeakMap,g=new WeakSet,Ue=function(){return this.is_fork||u(this,V).size>0},Gt=function(){for(const r of u(this,oe))for(const s of u(r,V).keys()){for(var t=!1,n=s;n.parent!==null;){if(u(this,B).has(n)){t=!0;break}n=n.parent}if(!t)return!0}return!1},Ve=function(){var a,f;if(xt++>1e3&&(W.delete(this),Xn()),!I(this,g,Ue).call(this)){for(const l of u(this,ee))u(this,$).delete(l),E(l,N),this.schedule(l);for(const l of u(this,$))E(l,z),this.schedule(l)}const t=u(this,P);ye(this,P,[]),this.apply();var n=Ee=[],r=[],s=qe=[];for(const l of t)try{I(this,g,at).call(this,l,n,r)}catch(c){throw $t(l),c}if(w=null,s.length>0){var i=We.ensure();for(const l of s)i.schedule(l)}if(Ee=null,qe=null,I(this,g,Ue).call(this)||I(this,g,Gt).call(this)){I(this,g,ot).call(this,r),I(this,g,ot).call(this,n);for(const[l,c]of u(this,B))Xt(l,c)}else{u(this,X).size===0&&W.delete(this),u(this,ee).clear(),u(this,$).clear();for(const l of u(this,me))l(this);u(this,me).clear(),kt(r),kt(n),(a=u(this,Ce))==null||a.resolve()}var o=w;if(u(this,P).length>0){const l=o??(o=this);u(l,P).push(...u(this,P).filter(c=>!u(l,P).includes(c)))}o!==null&&(W.add(o),I(f=o,g,Ve).call(f)),W.has(this)||I(this,g,zt).call(this)},at=function(t,n,r){t.f^=m;for(var s=t.first;s!==null;){var i=s.f,o=(i&(G|_e))!==0,a=o&&(i&m)!==0,f=a||(i&H)!==0||u(this,B).has(s);if(!f&&s.fn!==null){o?s.f^=m:(i&Se)!==0?n.push(s):He(s)&&((i&Z)!==0&&u(this,$).add(s),Oe(s));var l=s.first;if(l!==null){s=l;continue}}for(;s!==null;){var c=s.next;if(c!==null){s=c;break}s=s.parent}}},ot=function(t){for(var n=0;n!this.current.has(_));if(s.length===0)t&&d.discard();else if(n.length>0){d.activate();var i=new Set,o=new Map;for(var a of n)Kt(a,s,i,o);o=new Map;var f=[...d.current.keys()].filter(_=>this.current.has(_)?this.current.get(_)[0]!==_:!0);for(const _ of u(this,Fe))(_.f&(q|H|st))===0&&wt(_,f,o)&&((_.f&(Je|Z))!==0?(E(_,N),d.schedule(_)):u(d,ee).add(_));if(u(d,P).length>0){d.apply();for(var l of u(d,P))I(c=d,g,at).call(c,l,[],[]);ye(d,P,[])}d.deactivate()}}for(const d of W)u(d,oe).has(this)&&(u(d,oe).delete(this),u(d,oe).size===0&&!I(h=d,g,Ue).call(h)&&(d.activate(),I(v=d,g,Ve).call(v)))};let de=We;function Kn(e){var t=Me;Me=!0;try{for(var n;;){if(Un(),w===null)return n;w.flush()}}finally{Me=t}}function Xn(){try{Mn()}catch(e){Ke(e,ft)}}let j=null;function kt(e){var t=e.length;if(t!==0){for(var n=0;n0)){ne.clear();for(const s of j){if((s.f&(q|H))!==0)continue;const i=[s];let o=s.parent;for(;o!==null;)j.has(o)&&(j.delete(o),i.push(o)),o=o.parent;for(let a=i.length-1;a>=0;a--){const f=i[a];(f.f&(q|H))===0&&Oe(f)}}j.clear()}}j=null}}function Kt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const s of e.reactions){const i=s.f;(i&T)!==0?Kt(s,t,n,r):(i&(Je|Z))!==0&&(i&N)===0&&wt(s,t,r)&&(E(s,N),yt(s))}}function wt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const s of e.deps){if(Ae.call(t,s))return!0;if((s.f&T)!==0&&wt(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function yt(e){w.schedule(e)}function Xt(e,t){if(!((e.f&G)!==0&&(e.f&m)!==0)){(e.f&N)!==0?t.d.push(e):(e.f&z)!==0&&t.m.push(e),E(e,m);for(var n=e.first;n!==null;)Xt(n,t),n=n.next}}function $t(e){E(e,m);for(var t=e.first;t!==null;)$t(t),t=t.next}function $n(e,t,n,r){const s=Ye()?Et:Jn;var i=e.filter(v=>!v.settled);if(n.length===0&&i.length===0){r(t.map(s));return}var o=y,a=Zn(),f=i.length===1?i[0].promise:i.length>1?Promise.all(i.map(v=>v.promise)):null;function l(v){a();try{r(v)}catch(d){(o.f&q)===0&&Ke(d,o)}Xe()}if(n.length===0){f.then(()=>l(t.map(s)));return}var c=Zt();function h(){Promise.all(n.map(v=>Wn(v))).then(v=>l([...t.map(s),...v])).catch(v=>Ke(v,o)).finally(()=>c())}f?f.then(()=>{a(),h(),Xe()}):h()}function Zn(){var e=y,t=p,n=O,r=w;return function(i=!0){Ne(e),se(t),ze(n),i&&(e.f&q)===0&&(r==null||r.activate(),r==null||r.apply())}}function Xe(e=!0){Ne(null),se(null),ze(null),e&&(w==null||w.deactivate())}function Zt(){var e=y,t=e.b,n=w,r=t.is_rendered();return t.update_pending_count(1,n),n.increment(r,e),(s=!1)=>{t.update_pending_count(-1,n),n.decrement(r,e,s)}}function Et(e){var t=T|N,n=p!==null&&(p.f&T)!==0?p:null;return y!==null&&(y.f|=je),{ctx:O,deps:null,effects:null,equals:qt,f:t,fn:e,reactions:null,rv:0,v:A,wv:0,parent:n??y,ac:null}}function Wn(e,t,n){let r=y;r===null&&xn();var s=void 0,i=mt(A),o=!p,a=new Map;return ar(()=>{var d;var f=y,l=jt();s=l.promise;try{Promise.resolve(e()).then(l.resolve,l.reject).finally(Xe)}catch(_){l.reject(_),Xe()}var c=w;if(o){if((f.f&we)!==0)var h=Zt();if(r.b.is_rendered())(d=a.get(c))==null||d.reject(K),a.delete(c);else{for(const _ of a.values())_.reject(K);a.clear()}a.set(c,l)}const v=(_,x=void 0)=>{if(h){var R=x===K;h(R)}if(!(x===K||(f.f&q)!==0)){if(c.activate(),x)i.f|=te,ct(i,x);else{(i.f&te)!==0&&(i.f^=te),ct(i,_);for(const[xe,ke]of a){if(a.delete(xe),xe===c)break;ke.reject(K)}}c.deactivate()}};l.promise.then(v,_=>v(null,_||"unknown"))}),fr(()=>{for(const f of a.values())f.reject(K)}),new Promise(f=>{function l(c){function h(){c===s?f(i):l(s)}c.then(h,h)}l(s)})}function rs(e){const t=Et(e);return cn(t),t}function Jn(e){const t=Et(e);return t.equals=Ut,t}function Qn(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!Qt&&nr()}return t}function nr(){Qt=!1;for(const e of ut)(e.f&m)!==0&&E(e,z),He(e)&&Oe(e);ut.clear()}function is(e,t=1){var n=ge(e),r=t===1?n++:n--;return Q(e,n),r}function rt(e){Q(e,e.v+1)}function en(e,t,n){var r=e.reactions;if(r!==null)for(var s=Ye(),i=r.length,o=0;o{if(ce===i)return a();var f=p,l=ce;se(null),Ct(i);var c=a();return se(f),Ct(l),c};return r&&n.set("length",J(e.length)),new Proxy(e,{defineProperty(a,f,l){(!("value"in l)||l.configurable===!1||l.enumerable===!1||l.writable===!1)&&Pn();var c=n.get(f);return c===void 0?o(()=>{var h=J(l.value);return n.set(f,h),h}):Q(c,l.value,!0),!0},deleteProperty(a,f){var l=n.get(f);if(l===void 0){if(f in a){const c=o(()=>J(A));n.set(f,c),rt(s)}}else Q(l,A),rt(s);return!0},get(a,f,l){var d;if(f===ue)return e;var c=n.get(f),h=f in a;if(c===void 0&&(!h||(d=Ie(a,f))!=null&&d.writable)&&(c=o(()=>{var _=De(h?a[f]:A),x=J(_);return x}),n.set(f,c)),c!==void 0){var v=ge(c);return v===A?void 0:v}return Reflect.get(a,f,l)},getOwnPropertyDescriptor(a,f){var l=Reflect.getOwnPropertyDescriptor(a,f);if(l&&"value"in l){var c=n.get(f);c&&(l.value=ge(c))}else if(l===void 0){var h=n.get(f),v=h==null?void 0:h.v;if(h!==void 0&&v!==A)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return l},has(a,f){var v;if(f===ue)return!0;var l=n.get(f),c=l!==void 0&&l.v!==A||Reflect.has(a,f);if(l!==void 0||y!==null&&(!c||(v=Ie(a,f))!=null&&v.writable)){l===void 0&&(l=o(()=>{var d=c?De(a[f]):A,_=J(d);return _}),n.set(f,l));var h=ge(l);if(h===A)return!1}return c},set(a,f,l,c){var St;var h=n.get(f),v=f in a;if(r&&f==="length")for(var d=l;dJ(A)),n.set(d+"",_))}if(h===void 0)(!v||(St=Ie(a,f))!=null&&St.writable)&&(h=o(()=>J(void 0)),Q(h,De(l)),n.set(f,h));else{v=h.v!==A;var x=o(()=>De(l));Q(h,x)}var R=Reflect.getOwnPropertyDescriptor(a,f);if(R!=null&&R.set&&R.set.call(c,l),!v){if(r&&typeof f=="string"){var xe=n.get("length"),ke=Number(f);Number.isInteger(ke)&&ke>=xe.v&&Q(xe,ke+1)}rt(s)}return!0},ownKeys(a){ge(s);var f=Reflect.ownKeys(a).filter(h=>{var v=n.get(h);return v===void 0||v.v!==A});for(var[l,c]of n)c.v!==A&&!(l in a)&&f.push(l);return f},setPrototypeOf(){Cn()}})}function Dt(e){try{if(e!==null&&typeof e=="object"&&ue in e)return e[ue]}catch{}return e}function ls(e,t){return Object.is(Dt(e),Dt(t))}var It,rr,tn,nn;function fs(){if(It===void 0){It=window,rr=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;tn=Ie(t,"firstChild").get,nn=Ie(t,"nextSibling").get,Nt(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),Nt(n)&&(n.__t=void 0)}}function $e(e=""){return document.createTextNode(e)}function Ze(e){return tn.call(e)}function ie(e){return nn.call(e)}function as(e,t){if(!re)return Ze(e);var n=Ze(b);if(n===null)n=b.appendChild($e());else if(t&&n.nodeType!==Qe){var r=$e();return n==null||n.before(r),Re(r),r}return t&&bt(n),Re(n),n}function os(e,t=!1){if(!re){var n=Ze(e);return n instanceof Comment&&n.data===""?ie(n):n}if(t){if((b==null?void 0:b.nodeType)!==Qe){var r=$e();return b==null||b.before(r),Re(r),r}bt(b)}return b}function us(e,t=1,n=!1){let r=re?b:e;for(var s;t--;)s=r,r=ie(r);if(!re)return r;if(n){if((r==null?void 0:r.nodeType)!==Qe){var i=$e();return r===null?s==null||s.after(i):r.before(i),Re(i),i}bt(r)}return Re(r),r}function sr(e){e.textContent=""}function cs(){return!1}function _s(e,t,n){return document.createElementNS(t??Hn,e,void 0)}function bt(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===Qe;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function vs(e,t){if(t){const n=document.body;e.autofocus=!0,lt(()=>{document.activeElement===n&&e.focus()})}}function ds(e){re&&Ze(e)!==null&&sr(e)}let Mt=!1;function ir(){Mt||(Mt=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function Tt(e){var t=p,n=y;se(null),Ne(null);try{return e()}finally{se(t),Ne(n)}}function hs(e,t,n,r=n){e.addEventListener(t,()=>Tt(n));const s=e.__on_r;s?e.__on_r=()=>{s(),r(!0)}:e.__on_r=()=>r(!0),ir()}function rn(e){y===null&&(p===null&&In(),Dn()),pe&&kn()}function lr(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function U(e,t){var n=y;n!==null&&(n.f&H)!==0&&(e|=H);var r={ctx:O,deps:null,nodes:null,f:e|N|F,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};w==null||w.register_created_effect(r);var s=r;if((e&Se)!==0)Ee!==null?Ee.push(r):de.ensure().schedule(r);else if(t!==null){try{Oe(r)}catch(o){throw he(r),o}s.deps===null&&s.teardown===null&&s.nodes===null&&s.first===s.last&&(s.f&je)===0&&(s=s.first,(e&Z)!==0&&(e&Ge)!==0&&s!==null&&(s.f|=Ge))}if(s!==null&&(s.parent=n,n!==null&&lr(s,n),p!==null&&(p.f&T)!==0&&(e&_e)===0)){var i=p;(i.effects??(i.effects=[])).push(s)}return r}function sn(){return p!==null&&!Y}function fr(e){const t=U(Le,null);return E(t,m),t.teardown=e,t}function ps(e){rn();var t=y.f,n=!p&&(t&G)!==0&&(t&we)===0;if(n){var r=O;(r.e??(r.e=[])).push(e)}else return ln(e)}function ln(e){return U(Se|Yt,e)}function ws(e){return rn(),U(Le|Yt,e)}function ys(e){de.ensure();const t=U(_e|je,e);return(n={})=>new Promise(r=>{n.outro?cr(t,()=>{he(t),r(void 0)}):(he(t),r(void 0))})}function Es(e){return U(Se,e)}function ar(e){return U(Je|je,e)}function gs(e,t=0){return U(Le|t,e)}function ms(e,t=[],n=[],r=[]){$n(r,t,n,s=>{U(Le,()=>e(...s.map(ge)))})}function bs(e,t=0){var n=U(Z|t,e);return n}function Ts(e,t=0){var n=U(vt|t,e);return n}function As(e){return U(G|je,e)}function fn(e){var t=e.teardown;if(t!==null){const n=pe,r=p;Pt(!0),se(null);try{t.call(null)}finally{Pt(n),se(r)}}}function At(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&Tt(()=>{s.abort(K)});var r=n.next;(n.f&_e)!==0?n.parent=null:he(n,t),n=r}}function or(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&G)===0&&he(t),t=n}}function he(e,t=!0){var n=!1;(t||(e.f&On)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(ur(e.nodes.start,e.nodes.end),n=!0),E(e,Ot),At(e,t&&!n),Pe(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const i of r)i.stop();fn(e),e.f^=Ot,e.f|=q;var s=e.parent;s!==null&&s.first!==null&&an(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function ur(e,t){for(;e!==null;){var n=e===t?null:ie(e);e.remove(),e=n}}function an(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function cr(e,t,n=!0){var r=[];on(e,r,!0);var s=()=>{n&&he(e),t&&t()},i=r.length;if(i>0){var o=()=>--i||s();for(var a of r)a.out(o)}else s()}function on(e,t,n){if((e.f&H)===0){e.f^=H;var r=e.nodes&&e.nodes.t;if(r!==null)for(const a of r)(a.is_global||n)&&t.push(a);for(var s=e.first;s!==null;){var i=s.next,o=(s.f&Ge)!==0||(s.f&G)!==0&&(e.f&Z)!==0;on(s,t,o?n:!1),s=i}}}function Ss(e){un(e,!0)}function un(e,t){if((e.f&H)!==0){e.f^=H,(e.f&m)===0&&(E(e,N),de.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&Ge)!==0||(n.f&G)!==0;un(n,s?t:!1),n=r}var i=e.nodes&&e.nodes.t;if(i!==null)for(const o of i)(o.is_global||t)&&o.in()}}function Rs(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:ie(n);t.append(n),n=s}}let Be=!1,pe=!1;function Pt(e){pe=e}let p=null,Y=!1;function se(e){p=e}let y=null;function Ne(e){y=e}let L=null;function cn(e){p!==null&&(L===null?L=[e]:L.push(e))}let D=null,M=0,C=null;function _r(e){C=e}let _n=1,ae=0,ce=ae;function Ct(e){ce=e}function vn(){return++_n}function He(e){var t=e.f;if((t&N)!==0)return!0;if(t&T&&(e.f&=~ve),(t&z)!==0){for(var n=e.deps,r=n.length,s=0;se.wv)return!0}(t&F)!==0&&S===null&&E(e,m)}return!1}function dn(e,t,n=!0){var r=e.reactions;if(r!==null&&!(L!==null&&Ae.call(L,e)))for(var s=0;s{e.ac.abort(K)}),e.ac=null);try{e.f|=it;var c=e.fn,h=c();e.f|=we;var v=e.deps,d=w==null?void 0:w.is_fork;if(D!==null){var _;if(d||Pe(e,M),v!==null&&M>0)for(v.length=M+D.length,_=0;_{throw TypeError(i)};var D=(i,e,s)=>e in i?B(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>D(i,typeof e!="symbol"?e+"":e,s),y=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(y(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(y(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{r as F,d as b,p as C,c as x,a as A,e as S,h as j,f as q,m as z,s as E}from"./VJhVTb1A.js";var h,n,r,u,p,_,v;class I{constructor(e,s=!0){w(this,"anchor");l(this,h,new Map);l(this,n,new Map);l(this,r,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,e=>{if(t(this,h).has(e)){var s=t(this,h).get(e),a=t(this,n).get(s);if(a)F(a),t(this,u).delete(s);else{var c=t(this,r).get(s);c&&(t(this,n).set(s,c.effect),t(this,r).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[f,o]of t(this,h)){if(t(this,h).delete(f),f===e)break;const d=t(this,r).get(o);d&&(b(d.effect),t(this,r).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,h).values()).includes(f)){var k=document.createDocumentFragment();z(o,k),k.append(x()),t(this,r).set(f,{effect:o,fragment:k})}else b(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!a?(t(this,u).add(f),C(o,d,!1)):d()}}});l(this,v,e=>{t(this,h).delete(e);const s=Array.from(t(this,h).values());for(const[a,c]of t(this,r))s.includes(a)||(b(c.effect),t(this,r).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=S,c=E();if(s&&!t(this,n).has(e)&&!t(this,r).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,r).set(e,{effect:A(()=>s(o)),fragment:f})}else t(this,n).set(e,A(()=>s(this.anchor)));if(t(this,h).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,r))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else j&&(this.anchor=q),t(this,_).call(this,a)}}h=new WeakMap,n=new WeakMap,r=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{I as B}; +var B=Object.defineProperty;var g=i=>{throw TypeError(i)};var D=(i,e,s)=>e in i?B(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>D(i,typeof e!="symbol"?e+"":e,s),y=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(y(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(y(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{r as F,d as b,p as C,c as x,a as A,e as S,h as j,f as q,m as z,s as E}from"./D2EIOFXK.js";var h,n,r,u,p,_,v;class I{constructor(e,s=!0){w(this,"anchor");l(this,h,new Map);l(this,n,new Map);l(this,r,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,e=>{if(t(this,h).has(e)){var s=t(this,h).get(e),a=t(this,n).get(s);if(a)F(a),t(this,u).delete(s);else{var c=t(this,r).get(s);c&&(t(this,n).set(s,c.effect),t(this,r).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[f,o]of t(this,h)){if(t(this,h).delete(f),f===e)break;const d=t(this,r).get(o);d&&(b(d.effect),t(this,r).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,h).values()).includes(f)){var k=document.createDocumentFragment();z(o,k),k.append(x()),t(this,r).set(f,{effect:o,fragment:k})}else b(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!a?(t(this,u).add(f),C(o,d,!1)):d()}}});l(this,v,e=>{t(this,h).delete(e);const s=Array.from(t(this,h).values());for(const[a,c]of t(this,r))s.includes(a)||(b(c.effect),t(this,r).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=S,c=E();if(s&&!t(this,n).has(e)&&!t(this,r).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,r).set(e,{effect:A(()=>s(o)),fragment:f})}else t(this,n).set(e,A(()=>s(this.anchor)));if(t(this,h).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,r))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else j&&(this.anchor=q),t(this,_).call(this,a)}}h=new WeakMap,n=new WeakMap,r=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{I as B}; diff --git a/frontend/dist/_app/immutable/chunks/zf4GFcxs.js b/frontend/dist/_app/immutable/chunks/zf4GFcxs.js deleted file mode 100644 index b37d108..0000000 --- a/frontend/dist/_app/immutable/chunks/zf4GFcxs.js +++ /dev/null @@ -1 +0,0 @@ -import{b as N,h as S,J as L,E as Y,N as B,O as M,L as U,P as w,f as j,Q as q,i as z,S as H,U as J,k as m,F as O,V as Q,W as I,X as E,Y as V,Z as F,o as P,_ as W,$ as X,a0 as k,a1 as ee,a2 as re,w as ne,a3 as se,a4 as ie,t as ae,a5 as te,a6 as fe,a7 as ue,a8 as g,a9 as y,aa as v,ab as K}from"./VJhVTb1A.js";import{B as $}from"./U5yjBaoU.js";let b=!1;function le(e){var r=b;try{return b=!1,[e(),b]}finally{b=r}}function ve(e,r,s=!1){var n;S&&(n=j,L());var i=new $(e),t=s?Y:0;function o(a,f){if(S){var c=B(n);if(a!==parseInt(c.substring(1))){var u=M();U(u),i.anchor=u,w(!1),i.ensure(a,f),w(!0);return}}i.ensure(a,f)}N(()=>{var a=!1;r((f,c=0)=>{a=!0,o(c,f)}),a||o(-1,null)},t)}function he(e,r,s){var n;S&&(n=j,L());var i=new $(e);N(()=>{var t=r()??null;if(S){var o=B(n),a=o===q,f=t!==null;if(a!==f){var c=M();U(c),i.anchor=c,w(!1),i.ensure(t,t&&(u=>s(u,t))),w(!0);return}}i.ensure(t,t&&(u=>s(u,t)))},Y)}function D(e,r){return e===r||(e==null?void 0:e[I])===r}function be(e={},r,s,n){var i=z.r,t=O;return H(()=>{var o,a;return J(()=>{o=a,a=[],m(()=>{e!==s(...a)&&(r(e,...a),o&&D(s(...o),e)&&r(null,...o))})}),()=>{let f=t;for(;f!==i&&f.parent!==null&&f.parent.f&Q;)f=f.parent;const c=()=>{a&&D(s(...a),e)&&r(null,...a)},u=f.teardown;f.teardown=()=>{c(),u==null||u()}}}),e}const oe={get(e,r){if(!e.exclude.includes(r))return P(e.version),r in e.special?e.special[r]():e.props[r]},set(e,r,s){if(!(r in e.special)){var n=O;try{y(e.parent_effect),e.special[r]=pe({get[r](){return e.props[r]}},r,F)}finally{y(n)}}return e.special[r](s),g(e.version),!0},getOwnPropertyDescriptor(e,r){if(!e.exclude.includes(r)&&r in e.props)return{enumerable:!0,configurable:!0,value:e.props[r]}},deleteProperty(e,r){return e.exclude.includes(r)||(e.exclude.push(r),g(e.version)),!0},has(e,r){return e.exclude.includes(r)?!1:r in e.props},ownKeys(e){return Reflect.ownKeys(e.props).filter(r=>!e.exclude.includes(r))}};function Pe(e,r){return new Proxy({props:e,exclude:r,special:{},version:ue(0),parent_effect:O},oe)}const ce={get(e,r){let s=e.props.length;for(;s--;){let n=e.props[s];if(v(n)&&(n=n()),typeof n=="object"&&n!==null&&r in n)return n[r]}},set(e,r,s){let n=e.props.length;for(;n--;){let i=e.props[n];v(i)&&(i=i());const t=E(i,r);if(t&&t.set)return t.set(s),!0}return!1},getOwnPropertyDescriptor(e,r){let s=e.props.length;for(;s--;){let n=e.props[s];if(v(n)&&(n=n()),typeof n=="object"&&n!==null&&r in n){const i=E(n,r);return i&&!i.configurable&&(i.configurable=!0),i}}},has(e,r){if(r===I||r===K)return!1;for(let s of e.props)if(v(s)&&(s=s()),s!=null&&r in s)return!0;return!1},ownKeys(e){const r=[];for(let s of e.props)if(v(s)&&(s=s()),!!s){for(const n in s)r.includes(n)||r.push(n);for(const n of Object.getOwnPropertySymbols(s))r.includes(n)||r.push(n)}return r}};function Se(...e){return new Proxy({props:e},ce)}function pe(e,r,s,n){var x;var i=!ne||(s&se)!==0,t=(s&re)!==0,o=(s&fe)!==0,a=n,f=!0,c=()=>(f&&(f=!1,a=o?m(n):n),a);let u;if(t){var C=I in e||K in e;u=((x=E(e,r))==null?void 0:x.set)??(C&&r in e?l=>e[r]=l:void 0)}var d,T=!1;t?[d,T]=le(()=>e[r]):d=e[r],d===void 0&&n!==void 0&&(d=c(),u&&(i&&V(),u(d)));var p;if(i?p=()=>{var l=e[r];return l===void 0?c():(f=!0,l)}:p=()=>{var l=e[r];return l!==void 0&&(a=void 0),l===void 0?a:l},i&&(s&F)===0)return p;if(u){var G=e.$$legacy;return(function(l,h){return arguments.length>0?((!i||!h||G||T)&&u(h?p():l),l):p()})}var R=!1,_=((s&ie)!==0?ae:te)(()=>(R=!1,p()));t&&P(_);var Z=O;return(function(l,h){if(arguments.length>0){const A=h?P(_):i&&t?W(l):l;return X(_,A),R=!0,a!==void 0&&(a=A),l}return k&&R||(Z.f&ee)!==0?_.v:P(_)})}export{be as b,he as c,ve as i,Pe as l,pe as p,Se as s}; diff --git a/frontend/dist/_app/immutable/entry/app.CLLGSh1d.js b/frontend/dist/_app/immutable/entry/app.CLLGSh1d.js new file mode 100644 index 0000000..6a39d06 --- /dev/null +++ b/frontend/dist/_app/immutable/entry/app.CLLGSh1d.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BKQJkT-6.js","../chunks/IT17IssP.js","../chunks/D2EIOFXK.js","../chunks/D3-3j-uT.js","../chunks/lwvwhigX.js","../nodes/1.DViJQCF5.js","../chunks/C4rlClDa.js","../chunks/B-WYSyZB.js","../chunks/C1tKzvKE.js","../nodes/2.D9x-OSY6.js","../chunks/Dy37i2LO.js","../assets/2.Bl1bTUX7.css"])))=>i.map(i=>d[i]); +var G=t=>{throw TypeError(t)};var H=(t,e,s)=>e.has(t)||G("Cannot "+s);var l=(t,e,s)=>(H(t,e,"read from private field"),s?s.call(t):e.get(t)),j=(t,e,s)=>e.has(t)?G("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,s),A=(t,e,s,o)=>(H(t,e,"write to private field"),o?o.call(t,s):e.set(t,s),s);import{a0 as S,a9 as N,D as g,aH as Q,au as X,aI as Z,I as $,A as ee,u as te,aJ as re,k as L,N as se,K as ae,aK as C,L as ne,M as oe,aL as I,J as ce}from"../chunks/D2EIOFXK.js";import{h as ie,m as le,u as ue,s as fe}from"../chunks/C4rlClDa.js";import{a as w,c as D,f as Y,t as me}from"../chunks/IT17IssP.js";import{o as de}from"../chunks/C1tKzvKE.js";import{p as T,i as B,c as V,b as q}from"../chunks/Dy37i2LO.js";function he(t){return class extends _e{constructor(e){super({component:t,...e})}}}var y,m;class _e{constructor(e){j(this,y);j(this,m);var _;var s=new Map,o=(a,r)=>{var c=Z(r,!1,!1);return s.set(a,c),c};const u=new Proxy({...e.props||{},$$events:{}},{get(a,r){return g(s.get(r)??o(r,Reflect.get(a,r)))},has(a,r){return r===N?!0:(g(s.get(r)??o(r,Reflect.get(a,r))),Reflect.has(a,r))},set(a,r,c){return S(s.get(r)??o(r,c),c),Reflect.set(a,r,c)}});A(this,m,(e.hydrate?ie:le)(e.component,{target:e.target,anchor:e.anchor,props:u,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError})),(!((_=e==null?void 0:e.props)!=null&&_.$$host)||e.sync===!1)&&Q(),A(this,y,u.$$events);for(const a of Object.keys(l(this,m)))a==="$set"||a==="$destroy"||a==="$on"||X(this,a,{get(){return l(this,m)[a]},set(r){l(this,m)[a]=r},enumerable:!0});l(this,m).$set=a=>{Object.assign(u,a)},l(this,m).$destroy=()=>{ue(l(this,m))}}$set(e){l(this,m).$set(e)}$on(e,s){l(this,y)[e]=l(this,y)[e]||[];const o=(...u)=>s.call(this,...u);return l(this,y)[e].push(o),()=>{l(this,y)[e]=l(this,y)[e].filter(u=>u!==o)}}$destroy(){l(this,m).$destroy()}}y=new WeakMap,m=new WeakMap;const ve="modulepreload",ge=function(t,e){return new URL(t,e).href},W={},J=function(e,s,o){let u=Promise.resolve();if(s&&s.length>0){let a=function(i){return Promise.all(i.map(v=>Promise.resolve(v).then(b=>({status:"fulfilled",value:b}),b=>({status:"rejected",reason:b}))))};const r=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),k=(c==null?void 0:c.nonce)||(c==null?void 0:c.getAttribute("nonce"));u=a(s.map(i=>{if(i=ge(i,o),i in W)return;W[i]=!0;const v=i.endsWith(".css"),b=v?'[rel="stylesheet"]':"";if(!!o)for(let n=r.length-1;n>=0;n--){const f=r[n];if(f.href===i&&(!v||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${i}"]${b}`))return;const h=document.createElement("link");if(h.rel=v?"stylesheet":ve,v||(h.as="script"),h.crossOrigin="",h.href=i,k&&h.setAttribute("nonce",k),document.head.appendChild(h),v)return new Promise((n,f)=>{h.addEventListener("load",n),h.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${i}`)))})}))}function _(a){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=a,window.dispatchEvent(r),!r.defaultPrevented)throw a}return u.then(a=>{for(const r of a||[])r.status==="rejected"&&_(r.reason);return e().catch(_)})},je={};var ye=Y('
'),be=Y(" ",1);function Ee(t,e){$(e,!0);let s=T(e,"components",23,()=>[]),o=T(e,"data_0",3,null),u=T(e,"data_1",3,null);ee(()=>e.stores.page.set(e.page)),te(()=>{e.stores,e.page,e.constructors,s(),e.form,o(),u(),e.stores.page.notify()});let _=C(!1),a=C(!1),r=C(null);de(()=>{const n=e.stores.page.subscribe(()=>{g(_)&&(S(a,!0),re().then(()=>{S(r,document.title||"untitled page",!0)}))});return S(_,!0),n});const c=I(()=>e.constructors[1]);var k=be(),i=L(k);{var v=n=>{const f=I(()=>e.constructors[0]);var E=D(),O=L(E);V(O,()=>g(f),(P,R)=>{q(R(P,{get data(){return o()},get form(){return e.form},get params(){return e.page.params},children:(d,Re)=>{var U=D(),z=L(U);V(z,()=>g(c),(F,M)=>{q(M(F,{get data(){return u()},get form(){return e.form},get params(){return e.page.params}}),x=>s()[1]=x,()=>{var x;return(x=s())==null?void 0:x[1]})}),w(d,U)},$$slots:{default:!0}}),d=>s()[0]=d,()=>{var d;return(d=s())==null?void 0:d[0]})}),w(n,E)},b=n=>{const f=I(()=>e.constructors[0]);var E=D(),O=L(E);V(O,()=>g(f),(P,R)=>{q(R(P,{get data(){return o()},get form(){return e.form},get params(){return e.page.params}}),d=>s()[0]=d,()=>{var d;return(d=s())==null?void 0:d[0]})}),w(n,E)};B(i,n=>{e.constructors[1]?n(v):n(b,-1)})}var K=se(i,2);{var h=n=>{var f=ye(),E=ne(f);{var O=P=>{var R=me();ce(()=>fe(R,g(r))),w(P,R)};B(E,P=>{g(a)&&P(O)})}oe(f),w(n,f)};B(K,n=>{g(_)&&n(h)})}w(t,k),ae()}const Ae=he(Ee),Ce=[()=>J(()=>import("../nodes/0.BKQJkT-6.js"),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>J(()=>import("../nodes/1.DViJQCF5.js"),__vite__mapDeps([5,1,2,6,7,8]),import.meta.url),()=>J(()=>import("../nodes/2.D9x-OSY6.js"),__vite__mapDeps([9,1,2,8,10,4,6,3,11]),import.meta.url)],Ie=[],De={"/":[2]},p={handleError:(({error:t})=>{console.error(t)}),reroute:(()=>{}),transport:{}},Pe=Object.fromEntries(Object.entries(p.transport).map(([t,e])=>[t,e.decode])),Te=Object.fromEntries(Object.entries(p.transport).map(([t,e])=>[t,e.encode])),Be=!1,Ve=(t,e)=>Pe[t](e);export{Ve as decode,Pe as decoders,De as dictionary,Te as encoders,Be as hash,p as hooks,je as matchers,Ce as nodes,Ae as root,Ie as server_loads}; diff --git a/frontend/dist/_app/immutable/entry/app.Cn2ZzZiT.js b/frontend/dist/_app/immutable/entry/app.Cn2ZzZiT.js deleted file mode 100644 index 5c2d84c..0000000 --- a/frontend/dist/_app/immutable/entry/app.Cn2ZzZiT.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.DecrOZwe.js","../chunks/C14P8jaG.js","../chunks/VJhVTb1A.js","../chunks/CiUbu-1F.js","../chunks/U5yjBaoU.js","../nodes/1.DU6Ijmpv.js","../chunks/DmjnTBOd.js","../chunks/BeON-0X8.js","../chunks/BI00lc_u.js","../chunks/Cz6m6K8p.js","../nodes/2.DQxkgq4x.js","../chunks/zf4GFcxs.js","../assets/2.DKE6ochW.css"])))=>i.map(i=>d[i]); -var H=t=>{throw TypeError(t)};var J=(t,e,s)=>e.has(t)||H("Cannot "+s);var l=(t,e,s)=>(J(t,e,"read from private field"),s?s.call(t):e.get(t)),S=(t,e,s)=>e.has(t)?H("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,s),C=(t,e,s,o)=>(J(t,e,"write to private field"),o?o.call(t,s):e.set(t,s),s);import{$ as L,ab as X,o as g,aH as Z,au as M,aI as N,x as $,u as ee,j as te,aJ as re,g as j,C as se,z as ae,aK as A,A as ne,B as oe,aL as B,y as ce}from"../chunks/VJhVTb1A.js";import{h as ie,m as le,u as ue,s as fe}from"../chunks/BeON-0X8.js";import{a as w,c as I,f as W,t as me}from"../chunks/C14P8jaG.js";import{o as de}from"../chunks/Cz6m6K8p.js";import{p as T,i as D,c as V,b as q}from"../chunks/zf4GFcxs.js";function he(t){return class extends _e{constructor(e){super({component:t,...e})}}}var y,m;class _e{constructor(e){S(this,y);S(this,m);var _;var s=new Map,o=(a,r)=>{var c=N(r,!1,!1);return s.set(a,c),c};const u=new Proxy({...e.props||{},$$events:{}},{get(a,r){return g(s.get(r)??o(r,Reflect.get(a,r)))},has(a,r){return r===X?!0:(g(s.get(r)??o(r,Reflect.get(a,r))),Reflect.has(a,r))},set(a,r,c){return L(s.get(r)??o(r,c),c),Reflect.set(a,r,c)}});C(this,m,(e.hydrate?ie:le)(e.component,{target:e.target,anchor:e.anchor,props:u,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError})),(!((_=e==null?void 0:e.props)!=null&&_.$$host)||e.sync===!1)&&Z(),C(this,y,u.$$events);for(const a of Object.keys(l(this,m)))a==="$set"||a==="$destroy"||a==="$on"||M(this,a,{get(){return l(this,m)[a]},set(r){l(this,m)[a]=r},enumerable:!0});l(this,m).$set=a=>{Object.assign(u,a)},l(this,m).$destroy=()=>{ue(l(this,m))}}$set(e){l(this,m).$set(e)}$on(e,s){l(this,y)[e]=l(this,y)[e]||[];const o=(...u)=>s.call(this,...u);return l(this,y)[e].push(o),()=>{l(this,y)[e]=l(this,y)[e].filter(u=>u!==o)}}$destroy(){l(this,m).$destroy()}}y=new WeakMap,m=new WeakMap;const ve="modulepreload",ge=function(t,e){return new URL(t,e).href},K={},U=function(e,s,o){let u=Promise.resolve();if(s&&s.length>0){let a=function(i){return Promise.all(i.map(v=>Promise.resolve(v).then(b=>({status:"fulfilled",value:b}),b=>({status:"rejected",reason:b}))))};const r=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),k=(c==null?void 0:c.nonce)||(c==null?void 0:c.getAttribute("nonce"));u=a(s.map(i=>{if(i=ge(i,o),i in K)return;K[i]=!0;const v=i.endsWith(".css"),b=v?'[rel="stylesheet"]':"";if(!!o)for(let n=r.length-1;n>=0;n--){const f=r[n];if(f.href===i&&(!v||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${i}"]${b}`))return;const h=document.createElement("link");if(h.rel=v?"stylesheet":ve,v||(h.as="script"),h.crossOrigin="",h.href=i,k&&h.setAttribute("nonce",k),document.head.appendChild(h),v)return new Promise((n,f)=>{h.addEventListener("load",n),h.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${i}`)))})}))}function _(a){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=a,window.dispatchEvent(r),!r.defaultPrevented)throw a}return u.then(a=>{for(const r of a||[])r.status==="rejected"&&_(r.reason);return e().catch(_)})},Se={};var ye=W('
'),be=W(" ",1);function Ee(t,e){$(e,!0);let s=T(e,"components",23,()=>[]),o=T(e,"data_0",3,null),u=T(e,"data_1",3,null);ee(()=>e.stores.page.set(e.page)),te(()=>{e.stores,e.page,e.constructors,s(),e.form,o(),u(),e.stores.page.notify()});let _=A(!1),a=A(!1),r=A(null);de(()=>{const n=e.stores.page.subscribe(()=>{g(_)&&(L(a,!0),re().then(()=>{L(r,document.title||"untitled page",!0)}))});return L(_,!0),n});const c=B(()=>e.constructors[1]);var k=be(),i=j(k);{var v=n=>{const f=B(()=>e.constructors[0]);var E=I(),x=j(E);V(x,()=>g(f),(P,R)=>{q(R(P,{get data(){return o()},get form(){return e.form},get params(){return e.page.params},children:(d,Re)=>{var G=I(),p=j(G);V(p,()=>g(c),(F,Q)=>{q(Q(F,{get data(){return u()},get form(){return e.form},get params(){return e.page.params}}),O=>s()[1]=O,()=>{var O;return(O=s())==null?void 0:O[1]})}),w(d,G)},$$slots:{default:!0}}),d=>s()[0]=d,()=>{var d;return(d=s())==null?void 0:d[0]})}),w(n,E)},b=n=>{const f=B(()=>e.constructors[0]);var E=I(),x=j(E);V(x,()=>g(f),(P,R)=>{q(R(P,{get data(){return o()},get form(){return e.form},get params(){return e.page.params}}),d=>s()[0]=d,()=>{var d;return(d=s())==null?void 0:d[0]})}),w(n,E)};D(i,n=>{e.constructors[1]?n(v):n(b,-1)})}var z=se(i,2);{var h=n=>{var f=ye(),E=ne(f);{var x=P=>{var R=me();ce(()=>fe(R,g(r))),w(P,R)};D(E,P=>{g(a)&&P(x)})}oe(f),w(n,f)};D(z,n=>{g(_)&&n(h)})}w(t,k),ae()}const Ce=he(Ee),Ae=[()=>U(()=>import("../nodes/0.DecrOZwe.js"),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>U(()=>import("../nodes/1.DU6Ijmpv.js"),__vite__mapDeps([5,1,2,6,7,8,9]),import.meta.url),()=>U(()=>import("../nodes/2.DQxkgq4x.js"),__vite__mapDeps([10,1,2,9,11,4,7,3,6,12]),import.meta.url)],Be=[],Ie={"/":[2]},Y={handleError:(({error:t})=>{console.error(t)}),reroute:(()=>{}),transport:{}},Pe=Object.fromEntries(Object.entries(Y.transport).map(([t,e])=>[t,e.decode])),Te=Object.fromEntries(Object.entries(Y.transport).map(([t,e])=>[t,e.encode])),De=!1,Ve=(t,e)=>Pe[t](e);export{Ve as decode,Pe as decoders,Ie as dictionary,Te as encoders,De as hash,Y as hooks,Se as matchers,Ae as nodes,Ce as root,Be as server_loads}; diff --git a/frontend/dist/_app/immutable/entry/start.CX0ijzGM.js b/frontend/dist/_app/immutable/entry/start.CX0ijzGM.js new file mode 100644 index 0000000..dc07076 --- /dev/null +++ b/frontend/dist/_app/immutable/entry/start.CX0ijzGM.js @@ -0,0 +1 @@ +import{l as o,a as r}from"../chunks/B-WYSyZB.js";export{o as load_css,r as start}; diff --git a/frontend/dist/_app/immutable/entry/start.Ca27w5R7.js b/frontend/dist/_app/immutable/entry/start.Ca27w5R7.js deleted file mode 100644 index 2f7d149..0000000 --- a/frontend/dist/_app/immutable/entry/start.Ca27w5R7.js +++ /dev/null @@ -1 +0,0 @@ -import{l as o,a as r}from"../chunks/BI00lc_u.js";export{o as load_css,r as start}; diff --git a/frontend/dist/_app/immutable/nodes/0.DecrOZwe.js b/frontend/dist/_app/immutable/nodes/0.BKQJkT-6.js similarity index 63% rename from frontend/dist/_app/immutable/nodes/0.DecrOZwe.js rename to frontend/dist/_app/immutable/nodes/0.BKQJkT-6.js index 76ed193..5493acd 100644 --- a/frontend/dist/_app/immutable/nodes/0.DecrOZwe.js +++ b/frontend/dist/_app/immutable/nodes/0.BKQJkT-6.js @@ -1 +1 @@ -import{c as n,a}from"../chunks/C14P8jaG.js";import{g as s}from"../chunks/VJhVTb1A.js";import{s as p}from"../chunks/CiUbu-1F.js";const c=!1,i=!0,u=Object.freeze(Object.defineProperty({__proto__:null,prerender:i,ssr:c},Symbol.toStringTag,{value:"Module"}));function d(o,r){var e=n(),t=s(e);p(t,()=>r.children),a(o,e)}export{d as component,u as universal}; +import{c as n,a}from"../chunks/IT17IssP.js";import{k as s}from"../chunks/D2EIOFXK.js";import{s as p}from"../chunks/D3-3j-uT.js";const c=!1,i=!0,u=Object.freeze(Object.defineProperty({__proto__:null,prerender:i,ssr:c},Symbol.toStringTag,{value:"Module"}));function d(o,r){var e=n(),t=s(e);p(t,()=>r.children),a(o,e)}export{d as component,u as universal}; diff --git a/frontend/dist/_app/immutable/nodes/1.DU6Ijmpv.js b/frontend/dist/_app/immutable/nodes/1.DU6Ijmpv.js deleted file mode 100644 index 6885a46..0000000 --- a/frontend/dist/_app/immutable/nodes/1.DU6Ijmpv.js +++ /dev/null @@ -1 +0,0 @@ -import{a as g,f as h}from"../chunks/C14P8jaG.js";import{i as l}from"../chunks/DmjnTBOd.js";import{x as v,g as d,y as x,z as _,A as e,B as o,C as $}from"../chunks/VJhVTb1A.js";import{s as p}from"../chunks/BeON-0X8.js";import{s as k,p as m}from"../chunks/BI00lc_u.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var y=h("

",1);function j(f,n){v(n,!1),l();var t=y(),r=d(t),c=e(r,!0);o(r);var s=$(r,2),u=e(s,!0);o(s),x(()=>{var a;p(c,i.status),p(u,(a=i.error)==null?void 0:a.message)}),g(f,t),_()}export{j as component}; diff --git a/frontend/dist/_app/immutable/nodes/1.DViJQCF5.js b/frontend/dist/_app/immutable/nodes/1.DViJQCF5.js new file mode 100644 index 0000000..913fd08 --- /dev/null +++ b/frontend/dist/_app/immutable/nodes/1.DViJQCF5.js @@ -0,0 +1 @@ +import{a as b,f as k}from"../chunks/IT17IssP.js";import{g as x,A as y,u as i,j as $,B as l,C as j,D as v,F as A,G as B,H as C,I as D,k as E,J as F,K as G,L as u,M as m,N as H}from"../chunks/D2EIOFXK.js";import{s as g}from"../chunks/C4rlClDa.js";import{s as I,p as _}from"../chunks/B-WYSyZB.js";function J(a=!1){const t=x,e=t.l.u;if(!e)return;let r=()=>A(t.s);if(a){let o=0,s={};const c=B(()=>{let n=!1;const p=t.s;for(const f in p)p[f]!==s[f]&&(s[f]=p[f],n=!0);return n&&o++,o});r=()=>v(c)}e.b.length&&y(()=>{d(t,r),l(e.b)}),i(()=>{const o=$(()=>e.m.map(j));return()=>{for(const s of o)typeof s=="function"&&s()}}),e.a.length&&i(()=>{d(t,r),l(e.a)})}function d(a,t){if(a.l.s)for(const e of a.l.s)v(e);t()}C();const K={get error(){return _.error},get status(){return _.status}};I.updated.check;const h=K;var L=k("

",1);function z(a,t){D(t,!1),J();var e=L(),r=E(e),o=u(r,!0);m(r);var s=H(r,2),c=u(s,!0);m(s),F(()=>{var n;g(o,h.status),g(c,(n=h.error)==null?void 0:n.message)}),b(a,e),G()}export{z as component}; diff --git a/frontend/dist/_app/immutable/nodes/2.D9x-OSY6.js b/frontend/dist/_app/immutable/nodes/2.D9x-OSY6.js new file mode 100644 index 0000000..00fda76 --- /dev/null +++ b/frontend/dist/_app/immutable/nodes/2.D9x-OSY6.js @@ -0,0 +1,44 @@ +import{b as Cr,a as f,f as M,c as Ke,t as Ie,d as Ot}from"../chunks/IT17IssP.js";import{o as un,a as dr}from"../chunks/C1tKzvKE.js";import{h as Te,y as Ct,q as Lt,c as Sr,w as ir,b as oa,D as e,O as Ya,af as Qa,P as Mn,Q as zt,f as pt,ay as sa,aF as Ja,ao as Nn,e as ht,a as lr,aM as Za,s as eo,a6 as to,aN as ia,aE as dn,aO as ro,ab as An,aP as no,aQ as ao,aI as oo,aR as vt,aS as la,a2 as so,r as ca,p as ua,aT as Or,ad as da,aU as io,aV as lo,aC as co,m as uo,d as Rr,az as fa,J as Q,o as va,aW as fo,aG as vo,aA as go,n as ga,aX as ha,aY as ho,aZ as po,E as mo,aw as pa,U as Ar,j as fn,a_ as ma,a$ as _o,b0 as bo,b1 as _a,b2 as xo,b3 as yo,b4 as fr,b5 as wo,b6 as ko,b7 as Co,b8 as So,b9 as Po,ba as Ro,bb as Eo,aJ as St,V as $o,aK as F,$ as se,a0 as d,L as k,M as p,I as Me,k as Y,K as Ne,N as A,u as Ee,ap as Ce,aL as z,bc as Mo,bd as No,be as ba,bf as Ao,bg as Tn,bh as ot}from"../chunks/D2EIOFXK.js";import{p as fe,i as G,b as tt,c as To,r as We,s as Xe}from"../chunks/Dy37i2LO.js";import{i as Lo,a as Do,d as ie,b as Be,c as Fo,n as zo,e as Io,s as re,f as Ze}from"../chunks/C4rlClDa.js";import{s as ct}from"../chunks/D3-3j-uT.js";import{B as Oo}from"../chunks/lwvwhigX.js";function vn(t,r){return r}function Ho(t,r,a){for(var n=[],i=r.length,o,u=r.length,v=0;v{if(o){if(o.pending.delete(S),o.done.add(S),o.pending.size===0){var w=t.outrogroups;Yr(t,dn(o.done)),w.delete(o),w.size===0&&(t.outrogroups=null)}}else u-=1},!1)}if(u===0){var l=n.length===0&&a!==null;if(l){var y=a,b=y.parentNode;co(b),b.append(y),t.items.clear()}Yr(t,r,!l)}else o={pending:new Set(r),done:new Set},(t.outrogroups??(t.outrogroups=new Set)).add(o)}function Yr(t,r,a=!0){var n;if(t.pending.size>0){n=new Set;for(const u of t.pending.values())for(const v of u)n.add(t.items.get(v).e)}for(var i=0;i{var _=a();return ia(_)?_:_==null?[]:dn(_)}),w,T=new Map,N=!0;function x(_){(P.effect.f&so)===0&&(P.pending.delete(_),P.fallback=b,jo(P,w,u,r,n),b!==null&&(w.length===0?(b.f&vt)===0?ca(b):(b.f^=vt,or(b,null,u)):ua(b,()=>{b=null})))}function c(_){P.pending.delete(_)}var m=oa(()=>{w=e(S);var _=w.length;let R=!1;if(Te){var $=Ya(u)===Qa;$!==(_===0)&&(u=Mn(),Ct(u),zt(!1),R=!0)}for(var h=new Set,E=ht,D=eo(),L=0;L<_;L+=1){Te&&pt.nodeType===sa&&pt.data===Ja&&(u=pt,R=!0,zt(!1));var j=w[L],I=n(j,L),V=N?null:v.get(I);V?(V.v&&Nn(V.v,j),V.i&&Nn(V.i,L),D&&E.unskip_effect(V.e)):(V=Uo(v,N?u:Ln??(Ln=Sr()),j,I,L,i,r,a),N||(V.e.f|=vt),v.set(I,V)),h.add(I)}if(_===0&&o&&!b&&(N?b=lr(()=>o(u)):(b=lr(()=>o(Ln??(Ln=Sr()))),b.f|=vt)),_>h.size&&Za(),Te&&_>0&&Ct(Mn()),!N)if(T.set(E,h),D){for(const[O,B]of v)h.has(O)||E.skip_effect(B.e);E.oncommit(x),E.ondiscard(c)}else x(E);R&&zt(!0),e(S)}),P={effect:m,items:v,pending:T,outrogroups:null,fallback:b};N=!1,Te&&(u=pt)}function tr(t){for(;t!==null&&(t.f&io)===0;)t=t.next;return t}function jo(t,r,a,n,i){var j,I,V,O,B,U,q,g,s;var o=(n&lo)!==0,u=r.length,v=t.items,l=tr(t.effect.first),y,b=null,S,w=[],T=[],N,x,c,m;if(o)for(m=0;m0){var L=(n&la)!==0&&u===0?a:null;if(o){for(m=0;m{var C,H;if(S!==void 0)for(c of S)(H=(C=c.nodes)==null?void 0:C.a)==null||H.apply()})}function Uo(t,r,a,n,i,o,u,v){var l=(u&no)!==0?(u&ao)===0?oo(a,!1,!1):An(a):null,y=(u&ro)!==0?An(i):null;return{v:l,i:y,e:lr(()=>(o(r,l??a,y??i,v),()=>{t.delete(n)}))}}function or(t,r,a){if(t.nodes)for(var n=t.nodes.start,i=t.nodes.end,o=r&&(r.f&vt)===0?r.nodes.start:a;n!==null;){var u=fa(n);if(o.before(n),n===i)return;n=u}}function xt(t,r,a){r===null?t.effect.first=a:r.next=a,a===null?t.effect.last=r:a.prev=r}function Tr(t,r,a=!1,n=!1,i=!1,o=!1){var u=t,v="";if(a){var l=t;Te&&(u=Ct(Lt(l)))}Q(()=>{var y=va;if(v===(v=r()??"")){Te&&ir();return}if(a&&!Te){y.nodes=null,l.innerHTML=v,v!==""&&Cr(Lt(l),l.lastChild);return}if(y.nodes!==null&&(fo(y.nodes.start,y.nodes.end),y.nodes=null),v!==""){if(Te){pt.data;for(var b=ir(),S=b;b!==null&&(b.nodeType!==sa||b.data!=="");)S=b,b=fa(b);if(b===null)throw vo(),go;Cr(pt,S),u=Ct(b);return}var w=n?ha:i?ho:void 0,T=ga(n?"svg":i?"math":"template",w);T.innerHTML=v;var N=n||i?T:T.content;if(Cr(Lt(N),N.lastChild),n||i)for(;Lt(N);)u.before(Lt(N));else u.before(N)}})}function Vo(t,r,a,n,i,o){let u=Te;Te&&ir();var v=null;Te&&pt.nodeType===po&&(v=pt,ir());var l=Te?pt:t,y=new Oo(l,!1);oa(()=>{const b=r()||null;var S=ha;if(b===null){y.ensure(null,null);return}return y.ensure(b,w=>{if(b){if(v=Te?v:ga(b,S),Cr(v,v),n){Te&&Lo(b)&&v.append(document.createComment(""));var T=Te?Lt(v):v.appendChild(Sr());Te&&(T===null?zt(!1):Ct(T)),n(v,T)}va.nodes.end=v,w.before(v)}Te&&Ct(w)}),()=>{}},mo),pa(()=>{}),u&&(zt(!0),Ct(l))}function Bo(t,r,a){Ar(()=>{var n=fn(()=>r(t,a==null?void 0:a())||{});if(n!=null&&n.destroy)return()=>n.destroy()})}function qo(t,r){var a=void 0,n;ma(()=>{a!==(a=r())&&(n&&(Rr(n),n=null),a&&(n=lr(()=>{Ar(()=>a(t))})))})}function xa(t){var r,a,n="";if(typeof t=="string"||typeof t=="number")n+=t;else if(typeof t=="object")if(Array.isArray(t)){var i=t.length;for(r=0;r=0;){var v=u+o;(u===0||Dn.includes(n[u-1]))&&(v===n.length||Dn.includes(n[v]))?n=(u===0?"":n.substring(0,u))+n.substring(v+1):u=v}}return n===""?null:n}function Fn(t,r=!1){var a=r?" !important;":";",n="";for(var i of Object.keys(t)){var o=t[i];o!=null&&o!==""&&(n+=" "+i+": "+o+a)}return n}function Hr(t){return t[0]!=="-"||t[1]!=="-"?t.toLowerCase():t}function Go(t,r){if(r){var a="",n,i;if(Array.isArray(r)?(n=r[0],i=r[1]):n=r,t){t=String(t).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var o=!1,u=0,v=!1,l=[];n&&l.push(...Object.keys(n).map(Hr)),i&&l.push(...Object.keys(i).map(Hr));var y=0,b=-1;const x=t.length;for(var S=0;S{Er(t,t.__value)});r.observe(t,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),pa(()=>{r.disconnect()})}function zn(t,r,a=r){var n=new WeakSet,i=!0;_a(t,"change",o=>{var u=o?"[selected]":":checked",v;if(t.multiple)v=[].map.call(t.querySelectorAll(u),sr);else{var l=t.querySelector(u)??t.querySelector("option:not([disabled])");v=l&&sr(l)}a(v),t.__value=v,ht!==null&&n.add(ht)}),Ar(()=>{var o=r();if(t===document.activeElement){var u=ht;if(n.has(u))return}if(Er(t,o,i),i&&o===void 0){var v=t.querySelector(":checked");v!==null&&(o=sr(v),a(o))}t.__value=o,i=!1}),ya(t)}function sr(t){return"__value"in t?t.__value:t.value}const rr=Symbol("class"),nr=Symbol("style"),wa=Symbol("is custom element"),ka=Symbol("is html"),Xo=fr?"link":"LINK",Yo=fr?"input":"INPUT",Qo=fr?"option":"OPTION",Jo=fr?"select":"SELECT",Zo=fr?"progress":"PROGRESS";function qe(t){if(Te){var r=!1,a=()=>{if(!r){if(r=!0,t.hasAttribute("value")){var n=t.value;ye(t,"value",null),t.value=n}if(t.hasAttribute("checked")){var i=t.checked;ye(t,"checked",null),t.checked=i}}};t.__on_r=a,da(a),Co()}}function gn(t,r){var a=hn(t);a.value===(a.value=r??void 0)||t.value===r&&(r!==0||t.nodeName!==Zo)||(t.value=r??"")}function es(t,r){r?t.hasAttribute("selected")||t.setAttribute("selected",""):t.removeAttribute("selected")}function ye(t,r,a,n){var i=hn(t);Te&&(i[r]=t.getAttribute(r),r==="src"||r==="srcset"||r==="href"&&t.nodeName===Xo)||i[r]!==(i[r]=a)&&(r==="loading"&&(t[Ro]=a),a==null?t.removeAttribute(r):typeof a!="string"&&Ca(t).includes(r)?t[r]=a:t.setAttribute(r,a))}function ts(t,r,a,n,i=!1,o=!1){if(Te&&i&&t.nodeName===Yo){var u=t,v=u.type==="checkbox"?"defaultChecked":"defaultValue";v in a||qe(u)}var l=hn(t),y=l[wa],b=!l[ka];let S=Te&&y;S&&zt(!1);var w=r||{},T=t.nodeName===Qo;for(var N in r)N in a||(a[N]=null);a.class?a.class=dt(a.class):a[rr]&&(a.class=null),a[nr]&&(a.style??(a.style=null));var x=Ca(t);for(const h in a){let E=a[h];if(T&&h==="value"&&E==null){t.value=t.__value="",w[h]=E;continue}if(h==="class"){var c=t.namespaceURI==="http://www.w3.org/1999/xhtml";ke(t,c,E,n,r==null?void 0:r[rr],a[rr]),w[h]=E,w[rr]=a[rr];continue}if(h==="style"){Ge(t,E,r==null?void 0:r[nr],a[nr]),w[h]=E,w[nr]=a[nr];continue}var m=w[h];if(!(E===m&&!(E===void 0&&t.hasAttribute(h)))){w[h]=E;var P=h[0]+h[1];if(P!=="$$")if(P==="on"){const D={},L="$$"+h;let j=h.slice(2);var _=Io(j);if(Do(j)&&(j=j.slice(0,-7),D.capture=!0),!_&&m){if(E!=null)continue;t.removeEventListener(j,w[L],D),w[L]=null}if(_)ie(j,t,E),Be([j]);else if(E!=null){let I=function(V){w[h].call(this,V)};w[L]=Fo(j,t,I,D)}}else if(h==="style")ye(t,h,E);else if(h==="autofocus")wo(t,!!E);else if(!y&&(h==="__value"||h==="value"&&E!=null))t.value=t.__value=E;else if(h==="selected"&&T)es(t,E);else{var R=h;b||(R=zo(R));var $=R==="defaultValue"||R==="defaultChecked";if(E==null&&!y&&!$)if(l[h]=null,R==="value"||R==="checked"){let D=t;const L=r===void 0;if(R==="value"){let j=D.defaultValue;D.removeAttribute(R),D.defaultValue=j,D.value=D.__value=L?j:null}else{let j=D.defaultChecked;D.removeAttribute(R),D.defaultChecked=j,D.checked=L?j:!1}}else t.removeAttribute(h);else $||x.includes(R)&&(y||typeof E!="string")?(t[R]=E,R in l&&(l[R]=ko)):typeof E!="function"&&ye(t,R,E)}}}return S&&zt(!0),w}function In(t,r,a=[],n=[],i=[],o,u=!1,v=!1){xo(i,a,n,l=>{var y=void 0,b={},S=t.nodeName===Jo,w=!1;if(ma(()=>{var N=r(...l.map(e)),x=ts(t,y,N,o,u,v);w&&S&&"value"in N&&Er(t,N.value);for(let m of Object.getOwnPropertySymbols(b))N[m]||Rr(b[m]);for(let m of Object.getOwnPropertySymbols(N)){var c=N[m];m.description===yo&&(!y||c!==y[m])&&(b[m]&&Rr(b[m]),b[m]=lr(()=>qo(t,()=>c))),x[m]=c}y=x}),S){var T=t;Ar(()=>{Er(T,y.value,!0),ya(T)})}w=!0})}function hn(t){return t.__attributes??(t.__attributes={[wa]:t.nodeName.includes("-"),[ka]:t.namespaceURI===So})}var On=new Map;function Ca(t){var r=t.getAttribute("is")||t.nodeName,a=On.get(r);if(a)return a;On.set(r,a=[]);for(var n,i=t,o=Element.prototype;o!==i;){n=Eo(i);for(var u in n)n[u].set&&a.push(u);i=Po(i)}return a}function Je(t,r,a=r){var n=new WeakSet;_a(t,"input",async i=>{var o=i?t.defaultValue:t.value;if(o=Ur(t)?Vr(o):o,a(o),ht!==null&&n.add(ht),await St(),o!==(o=r())){var u=t.selectionStart,v=t.selectionEnd,l=t.value.length;if(t.value=o??"",v!==null){var y=t.value.length;u===v&&v===l&&y>l?(t.selectionStart=y,t.selectionEnd=y):(t.selectionStart=u,t.selectionEnd=Math.min(v,y))}}}),(Te&&t.defaultValue!==t.value||fn(r)==null&&t.value)&&(a(Ur(t)?Vr(t.value):t.value),ht!==null&&n.add(ht)),$o(()=>{var i=r();if(t===document.activeElement){var o=ht;if(n.has(o))return}Ur(t)&&i===Vr(t.value)||t.type==="date"&&!i&&!t.value||i!==t.value&&(t.value=i??"")})}function Ur(t){var r=t.type;return r==="number"||r==="range"}function Vr(t){return t===""?null:+t}let _t=F(se({type:"none",props:{},resolve:null}));function rs(){return e(_t)}function at(t=void 0){const{resolve:r}=e(_t);d(_t,{type:"none",props:{},resolve:null},!0),r==null||r(t)}function ns(t){return new Promise(r=>{d(_t,{type:"run",props:{cases:t},resolve:r},!0)})}function as(t){return new Promise(r=>{d(_t,{type:"restart",props:{cases:t},resolve:r},!0)})}function os(t){return new Promise(r=>{d(_t,{type:"clean",props:{cases:t},resolve:r},!0)})}function et(t,r="Alert"){return new Promise(a=>{d(_t,{type:"alert",props:{title:r,message:t},resolve:()=>a()},!0)})}function Hn(t,r="Confirm",a="OK",n="primary"){return new Promise(i=>{d(_t,{type:"confirm",props:{title:r,message:t,confirmLabel:a,confirmVariant:n},resolve:i},!0)})}function pn(t,r="",a="Input",n="",i=!1){return new Promise(o=>{d(_t,{type:"prompt",props:{title:a,message:t,value:r,placeholder:n,multiline:i},resolve:o},!0)})}var ss=M('
');function Xt(t,r){function a(o){return document.body.appendChild(o),{destroy(){o.remove()}}}var n=ss(),i=k(n);ct(i,()=>r.children),p(n),Bo(n,o=>a==null?void 0:a(o)),f(t,n)}var is=M('
'),ls=M('
');function vr(t,r){Me(r,!0);const a=N=>{var x=ls(),c=k(x),m=k(c),P=k(m),_=k(P,!0);p(P);var R=A(P,2);{var $=L=>{var j=is(),I=k(j,!0);p(j),Q(()=>re(I,r.subtitle)),f(L,j)};G(R,L=>{r.subtitle&&L($)})}p(m);var h=A(m,2);ct(h,()=>r.children);var E=A(h,2),D=k(E);ct(D,()=>r.footer),p(E),p(c),p(x),Q(()=>{ke(c,1,`w-[min(${n()??""},96vw)] bg-white border border-border rounded-[10px] p-4.5`),ye(c,"aria-labelledby",r.titleId),ye(P,"id",r.titleId),re(_,r.title)}),ie("keydown",x,u),ie("mousedown",x,l),ie("click",x,y),f(N,x)};let n=fe(r,"maxWidth",3,"520px"),i=fe(r,"portal",3,!1);function o(){r.onCancel?r.onCancel():at(null)}function u(N){N.key==="Escape"?o():N.key==="Enter"&&r.onConfirm&&r.onConfirm()}let v=!1;function l(N){v=N.target.dataset.backdrop!==void 0}function y(N){const x=N.target.dataset.backdrop!==void 0;v&&x&&o(),v=!1}var b=Ke(),S=Y(b);{var w=N=>{Xt(N,{children:(x,c)=>{a(x)}})},T=N=>{a(N)};G(S,N=>{i()?N(w):N(T,-1)})}f(t,b),Ne()}Be(["keydown","mousedown","click"]);var cs=M("");function Re(t,r){let a=fe(r,"variant",3,"primary"),n=fe(r,"size",3,"default"),i=fe(r,"disabled",3,!1);const o={default:"h-[34px] rounded-md px-3.5 leading-none text-[13px] tracking-wide",sm:"h-[30px] rounded-md px-2.5 leading-none text-xs"},u={primary:"bg-edf-bleu-moyen text-white border-none hover:bg-[rgb(12,72,170)]",secondary:"bg-white text-ink border border-border hover:bg-edf-gris-clair",run:"bg-edf-vert-fonce text-white border-none hover:bg-[rgb(38,98,12)]",warning:"bg-edf-orange-moyen text-white border-none hover:bg-[rgb(230,118,20)]",danger:"bg-edf-orange-fonce text-white border-none hover:bg-[rgb(180,56,8)]"};var v=cs(),l=k(v);ct(l,()=>r.children),p(v),Q(()=>{ke(v,1,`inline-flex items-center justify-center gap-1.5 font-bold cursor-pointer transition-colors duration-150 disabled:opacity-40 disabled:pointer-events-none ${o[n()]??""} ${u[a()]??""}`),v.disabled=i()}),ie("click",v,function(...y){var b;(b=r.onclick)==null||b.apply(this,y)}),f(t,v)}Be(["click"]);var us=M(" ",1),ds=M(''),fs=M(''),vs=M('
'),gs=M('

',1);function hs(t,r){Me(r,!0);let a=fe(r,"confirmLabel",3,"OK"),n=fe(r,"confirmVariant",3,"primary"),i=fe(r,"value",3,""),o=fe(r,"placeholder",3,""),u=fe(r,"multiline",3,!1);const v=i();let l=F(se(v)),y=F(void 0),b=F(void 0);Ee(()=>{var T;r.mode==="prompt"&&e(y)?(e(y).focus(),"select"in e(y)&&e(y).select()):e(b)&&((T=e(b).querySelector("button"))==null||T.focus())});function S(){r.mode==="prompt"?at(e(l)):r.mode==="confirm"?at(!0):at()}function w(){r.mode==="confirm"?at(!1):r.mode==="prompt"?at(null):at()}{const T=x=>{var c=us(),m=Y(c);{var P=$=>{Re($,{variant:"secondary",onclick:w,children:(h,E)=>{Ce();var D=Ie("Cancel");f(h,D)},$$slots:{default:!0}})};G(m,$=>{r.mode!=="alert"&&$(P)})}var _=A(m,2),R=k(_);Re(R,{get variant(){return n()},onclick:S,children:($,h)=>{Ce();var E=Ie();Q(()=>re(E,a())),f($,E)},$$slots:{default:!0}}),p(_),tt(_,$=>d(b,$),()=>e(b)),f(x,c)};let N=z(()=>u()?void 0:S);vr(t,{get title(){return r.title},titleId:"app-dialog-title",get onConfirm(){return e(N)},onCancel:w,footer:T,children:(x,c)=>{var m=gs(),P=Y(m),_=k(P,!0);p(P);var R=A(P,2);{var $=h=>{var E=vs(),D=k(E);{var L=I=>{var V=ds();Mo(V),tt(V,O=>d(y,O),()=>e(y)),Q(()=>ye(V,"placeholder",o())),Je(V,()=>e(l),O=>d(l,O)),f(I,V)},j=I=>{var V=fs();qe(V),tt(V,O=>d(y,O),()=>e(y)),Q(()=>ye(V,"placeholder",o())),Je(V,()=>e(l),O=>d(l,O)),f(I,V)};G(D,I=>{u()?I(L):I(j,-1)})}p(E),f(h,E)};G(R,h=>{r.mode==="prompt"&&h($)})}Q(()=>re(_,r.message)),f(x,m)},$$slots:{footer:!0,default:!0}})}Ne()}var ps=M('');function $e(t,r){var a=ps(),n=k(a),i=A(n);ct(i,()=>r.children),p(a),Q(()=>re(n,`${r.text??""} `)),f(t,a)}function wt(t,r){const a=localStorage.getItem(t);if(a===null)return r;const n=Number(a);return Number.isFinite(n)?n:r}function kt(t,r){Number.isFinite(r)&&localStorage.setItem(t,String(r))}function ms(t,r){return localStorage.getItem(t)??r}function _s(){return{n:wt("csauto_run_n",1),nt:wt("csauto_run_nt",1),maxParallel:wt("csauto_run_max_parallel",0)||null}}function bs(t){kt("csauto_run_n",t.n),kt("csauto_run_nt",t.nt),t.maxParallel&&kt("csauto_run_max_parallel",t.maxParallel)}function xs(){return{n:wt("csauto_restart_n",1),nt:wt("csauto_restart_nt",1),maxParallel:wt("csauto_restart_max_parallel",0)||null,mode:ms("csauto_restart_mode",""),value:wt("csauto_restart_value",100)}}function ys(t){kt("csauto_restart_n",t.n),kt("csauto_restart_nt",t.nt),t.maxParallel&&kt("csauto_restart_max_parallel",t.maxParallel),localStorage.setItem("csauto_restart_mode",t.mode),kt("csauto_restart_value",t.value)}function ws(){return{keepLast:wt("csauto_clean_keep_last",1)}}function ks(t){kt("csauto_clean_keep_last",t.keepLast)}var Cs=M(" ",1),Ss=M(''),Ps=M(''),Rs=M(''),Es=M('
');function $s(t,r){Me(r,!0);const a=_s(),n=a.maxParallel??(r.cases.length>1?r.cases.length:0);let i=F(se(a.n)),o=F(se(a.nt)),u=F(se(n));async function v(){if(!Number.isFinite(e(i))||e(i)<=0){await et("MPI ranks must be an integer > 0.","Invalid value");return}if(!Number.isFinite(e(o))||e(o)<=0){await et("Threads must be an integer > 0.","Invalid value");return}if(e(u)&&(!Number.isFinite(e(u))||e(u)<=0)){await et("Max parallel must be empty or > 0.","Invalid value");return}const l={n:e(i),nt:e(o),maxParallel:e(u)||null};bs(l),at(l)}{const l=S=>{var w=Cs(),T=Y(w);Re(T,{variant:"secondary",onclick:()=>at(null),children:(x,c)=>{Ce();var m=Ie("Cancel");f(x,m)},$$slots:{default:!0}});var N=A(T,2);Re(N,{variant:"run",onclick:v,children:(x,c)=>{Ce();var m=Ie("Run");f(x,m)},$$slots:{default:!0}}),f(S,w)};let y=z(()=>r.cases.length),b=z(()=>r.cases.length>1?"s":"");vr(t,{title:"Run Cases",titleId:"run-dialog-title",get subtitle(){return`${e(y)??""} case${e(b)??""} selected`},onConfirm:v,footer:l,children:(S,w)=>{var T=Es(),N=k(T);$e(N,{text:"MPI Ranks (n)",children:(m,P)=>{var _=Ss();qe(_),Je(_,()=>e(i),R=>d(i,R)),f(m,_)}});var x=A(N,2);$e(x,{text:"OMP Threads (nt)",children:(m,P)=>{var _=Ps();qe(_),Je(_,()=>e(o),R=>d(o,R)),f(m,_)}});var c=A(x,2);$e(c,{text:"Max Parallel",children:(m,P)=>{var _=Rs();qe(_),Je(_,()=>e(u),R=>d(u,R)),f(m,_)}}),p(T),f(S,T)},$$slots:{footer:!0,default:!0}})}Ne()}const Qr="csauto_token";let Sa=F(se(localStorage.getItem(Qr)??""));function mn(){return e(Sa)}function Pa(t){d(Sa,t,!0),t?localStorage.setItem(Qr,t):localStorage.removeItem(Qr)}let yr=null;async function _n(t,r={}){const a=new Headers(r.headers),n=mn();n&&a.set("X-CSAUTO-TOKEN",n);const i=await fetch(t,{...r,headers:a});if(i.status===401){yr||(yr=pn("API token required:",n,"Authentication"));const o=await yr;return yr=null,o===null?i:(Pa(o),a.set("X-CSAUTO-TOKEN",o),fetch(t,{...r,headers:a}))}return i}async function it(t){const r=await _n(t);if(!r.ok)throw new Error(`GET ${t} failed: ${r.status}`);return r.json()}async function bn(t){const r=await _n(t);if(!r.ok)throw new Error(`GET ${t} failed: ${r.status}`);return r.text()}async function Et(t,r){const a=await _n(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!a.ok){const n=await a.text().catch(()=>a.statusText);throw new Error(`POST ${t} failed (${a.status}): ${n}`)}return a.json()}function Lr(t){return t.map(r=>`case=${encodeURIComponent(r)}`).join("&")}function Ms(t=!1){return it(`/api/status${t?"?log=1":""}`)}function jn(t){return it(`/api/perf?${Lr(t)}`)}function Ns(){return it("/api/app_config")}function As(t){return`/api/solver_logo?solver=${encodeURIComponent(t)}`}function Ts(t){return`/api/solver_icon?solver=${encodeURIComponent(t)}`}function Ls(t){return it(`/api/residual_columns?${Lr(t)}`).then(r=>r.columns??[])}function Ds(t,r,a={}){const n=new URLSearchParams;return t.forEach(i=>n.append("case",i)),n.set("columns",r.join(",")),a.width&&n.set("width",String(a.width)),a.height&&n.set("height",String(a.height)),a.xMin!==void 0&&n.set("x_min",String(a.xMin)),a.includeHistory&&n.set("include_history","true"),bn(`/api/residuals_svg?${n}`)}function Ra(t){return it(`/api/restart_origin?${Lr(t)}`)}function Fs(t){var a;const r=new URLSearchParams;return t.cases.forEach(n=>r.append("case",n)),(a=t.files)!=null&&a.length&&r.set("files",t.files.join(",")),t.maxHits&&r.set("max_hits",String(t.maxHits)),t.context!==void 0&&r.set("context",String(t.context)),t.sev&&r.set("sev",t.sev),t.q&&r.set("q",t.q),it(`/api/recent_errors?${r}`)}function zs(t){return it(`/api/tail_files?case=${encodeURIComponent(t)}`).then(r=>r.files??[])}function Is(t,r,a){const n=new URLSearchParams({case:t,file:r,n:String(a)});return it(`/api/tail_lines?${n}`)}function Ea(t){return it(`/api/resu_dirs?${Lr(t)}`).then(r=>r.dirs??[])}function Jr(t,r){const a=new URLSearchParams({scope:r});return t.forEach(n=>a.append("case",n)),it(`/api/probes?${a}`).then(n=>n.files??[])}function Os(t,r){const a=new URLSearchParams;return t.forEach(n=>a.append("case",n)),r.forEach(n=>a.append("probe",n)),it(`/api/probe_columns?${a}`).then(n=>n.columns??[])}function Hs(t,r,a){const n=new URLSearchParams({case:t,probe:r});return a.forEach(i=>n.append("column",i)),it(`/api/probe_position?${n}`)}function js(t){const r=new URLSearchParams;return t.cases.forEach(a=>r.append("case",a)),t.probes.forEach(a=>r.append("probe",a)),r.set("columns",t.columns.join(",")),t.axis&&r.set("axis",t.axis),t.xMin!==void 0&&r.set("x_min",String(t.xMin)),t.timeMin!==void 0&&r.set("time_min",String(t.timeMin)),r.set("include_history","true"),t.width&&r.set("width",String(t.width)),t.height&&r.set("height",String(t.height)),bn(`/api/probe_svg?${r}`)}function Us(t){const r=new URLSearchParams;return t.cases.forEach(a=>r.append("case",a)),t.base&&r.set("base",t.base),t.kind&&r.set("kind",t.kind),t.filter&&r.set("filter",t.filter),bn(`/api/compare_runs?${r}`)}function Un(t){return Et("/api/run_case",{cases:t.cases,n:t.n,nt:t.nt,max_parallel:t.maxParallel??void 0,restart:t.restart??!1,restart_mode:t.restartMode??"",restart_value:t.restartValue??void 0,restart_path:t.restartPath||void 0}).then(()=>{})}function Vs(t){return Et("/api/kill_case",{cases:t}).then(()=>{})}function Bs(t){return Et("/api/control_case",{cases:t.cases,action:t.action,value:t.value??null}).then(()=>{})}function qs(t){return Et("/api/cleanup_cases",{cases:t.cases,keep_last:t.keepLast??1,prune_resu:t.pruneResu??!0,keep_resu:t.keepResu??[],delete_resu:t.deleteResu??[],max_log_mb:t.maxLogMb??50,clear_cid:!0,clear_pyc:!1})}function Ks(t,r){return Et("/api/case_note",{case:t,note:r}).then(()=>{})}function Ws(t,r){return Et("/api/case_convergence",{case:t,convergence:r}).then(()=>{})}function Gs(t){return Et("/api/open_gui",{case:t}).then(()=>{})}function Xs(){return it("/api/settings/telemetry")}function Ys(t){return Et("/api/settings/telemetry",{enabled:t})}const Qs=["status","tail","errors"],Js=3e4;let cr=F(null),Br=!1,qr=1e3,Vn;function Pt(){return e(cr)}async function $a(){if(!(e(cr)||Br)){clearTimeout(Vn),Br=!0;try{d(cr,await Ns(),!0)}catch{Vn=setTimeout($a,qr),qr=Math.min(qr*2,Js)}finally{Br=!1}}}function Nt(t){var r;return(((r=e(cr))==null?void 0:r.panels)??Qs).includes(t)}function Zr(t){var r;return((r=e(cr))==null?void 0:r.capabilities.includes(t))??!1}function xn(t){return t.map(r=>({value:r,label:r}))}function Ma(t,r){return Number.isFinite(r)?r<=0?`${t.value_label} must be greater than 0.`:t.value_kind==="int"&&!Number.isInteger(r)?`${t.value_label} must be a whole number.`:"":`${t.value_label} must be a number.`}var Zs=M(" ",1),ei=M(''),ti=M(''),ri=M(''),ni=M(""),ai=M(""),oi=M(''),si=M(""),ii=M(""),li=M('
',1);function ci(t,r){var N,x;Me(r,!0);const a=((N=Pt())==null?void 0:N.restart_modes)??[],n=xs(),i=n.maxParallel??(r.cases.length>1?r.cases.length:0);let o=F(se(n.n)),u=F(se(n.nt)),v=F(se(i)),l=F(se(a.some(c=>c.name===n.mode)?n.mode:((x=a[0])==null?void 0:x.name)??"")),y=F(se(n.value)),b=F(""),S=F(se([])),w=z(()=>a.find(c=>c.name===e(l)));un(async()=>{if(r.cases.length===1)try{d(S,await Ea(r.cases),!0)}catch{d(S,[],!0)}});async function T(){var P,_;if(!Number.isFinite(e(o))||e(o)<=0){await et("MPI ranks must be an integer > 0.","Invalid value");return}if(!Number.isFinite(e(u))||e(u)<=0){await et("Threads must be an integer > 0.","Invalid value");return}if(e(v)&&(!Number.isFinite(e(v))||e(v)<=0)){await et("Max parallel must be empty or > 0.","Invalid value");return}const c=(P=e(w))!=null&&P.value_label?Ma(e(w),e(y)):"";if(c){await et(c,"Invalid value");return}const m={n:e(o),nt:e(u),maxParallel:e(v)||null,restartMode:e(l),restartValue:(_=e(w))!=null&&_.value_label?e(y):null,restartPath:e(b)||null};ys({n:e(o),nt:e(u),maxParallel:e(v)||null,mode:e(l),value:e(y)}),at(m)}{const c=_=>{var R=Zs(),$=Y(R);Re($,{variant:"secondary",onclick:()=>at(null),children:(E,D)=>{Ce();var L=Ie("Cancel");f(E,L)},$$slots:{default:!0}});var h=A($,2);Re(h,{variant:"warning",onclick:T,children:(E,D)=>{Ce();var L=Ie("Restart");f(E,L)},$$slots:{default:!0}}),f(_,R)};let m=z(()=>r.cases.length),P=z(()=>r.cases.length>1?"s":"");vr(t,{title:"Restart Cases",titleId:"restart-dialog-title",get subtitle(){return`${e(m)??""} case${e(P)??""} selected`},onConfirm:T,footer:c,children:(_,R)=>{var $=li(),h=Y($),E=k(h);$e(E,{text:"MPI Ranks (n)",children:(q,g)=>{var s=ei();qe(s),Je(s,()=>e(o),C=>d(o,C)),f(q,s)}});var D=A(E,2);$e(D,{text:"OMP Threads (nt)",children:(q,g)=>{var s=ti();qe(s),Je(s,()=>e(u),C=>d(u,C)),f(q,s)}});var L=A(D,2);$e(L,{text:"Max Parallel",children:(q,g)=>{var s=ri();qe(s),Je(s,()=>e(v),C=>d(v,C)),f(q,s)}}),p(h);var j=A(h,2),I=k(j);{var V=q=>{$e(q,{text:"Mode",children:(g,s)=>{var C=ai();je(C,21,()=>a,H=>H.name,(H,W)=>{var ee=ni(),ne=k(ee,!0);p(ee);var me={};Q(()=>{re(ne,e(W).label),me!==(me=e(W).name)&&(ee.value=(ee.__value=e(W).name)??"")}),f(H,ee)}),p(C),zn(C,()=>e(l),H=>d(l,H)),f(g,C)}})};G(I,q=>{a.length>0&&q(V)})}var O=A(I,2);{var B=q=>{$e(q,{get text(){return e(w).value_label},children:(g,s)=>{var C=oi();qe(C),Q(()=>{ye(C,"min",e(w).value_kind==="int"?"1":"0"),ye(C,"step",e(w).value_kind==="int"?"1":"any")}),Je(C,()=>e(y),H=>d(y,H)),f(g,C)}})};G(O,q=>{var g;(g=e(w))!=null&&g.value_label&&q(B)})}var U=A(O,2);$e(U,{text:"Restart from",children:(q,g)=>{var s=ii(),C=k(s);C.value=C.__value="";var H=A(C);je(H,16,()=>e(S),W=>W,(W,ee)=>{var ne=si(),me=k(ne,!0);p(ne);var K={};Q(()=>{re(me,ee),K!==(K=ee)&&(ne.value=(ne.__value=ee)??"")}),f(W,ne)}),p(s),Q(()=>{s.disabled=r.cases.length>1,ye(s,"title",r.cases.length>1?"Each case restarts from its own latest run":void 0)}),zn(s,()=>e(b),W=>d(b,W)),f(q,s)}}),p(j),f(_,$)},$$slots:{footer:!0,default:!0}})}Ne()}function Le(t,r){let a=fe(r,"size",3,14);var n=Ke(),i=Y(n);To(i,()=>r.icon,(o,u)=>{u(o,{get size(){return a()},class:"icon"})}),f(t,n)}/** + * @file + * @license @lucide/svelte v1.52.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const At={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};/** + * @file + * @license @lucide/svelte v1.52.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const en=(...t)=>t.filter((r,a,n)=>!!r&&r.trim()!==""&&n.indexOf(r)===a).join(" ").trim();/** + * @file + * @license @lucide/svelte v1.52.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */function Kr(t){return t!=null}function ui(t,r={}){var w,T;const a=r.attributeNames??{},n=N=>a[N]??N,i=t.size??t.width??At.width,o=t.size??t.height??At.height,u=((w=t.aliases)==null?void 0:w.filter(N=>typeof N=="string"&&N.trim()!=="").map(N=>`lucide-${N}`))??[],v=[...t.name?[`lucide-${t.name}`]:[],...u],l=((T=r.className)==null?void 0:T.split(" ").filter(Boolean))??[],y=r.includeDefaultClasses===!1?en(...l):en("lucide",...v,...l),b=r.absoluteStrokeWidth?Number(r.strokeWidth??At["stroke-width"])*Number(t.size??t.width??At.width)/Number(r.size??r.width??At.width):r.strokeWidth??At["stroke-width"];return["svg",{...Object.entries(At).reduce((N,[x,c])=>(N[n(x)]=c,N),{}),..."color"in r&&r.color&&{[n("stroke")]:r.color},..."size"in r&&Kr(r.size)&&{[n("width")]:r.size,[n("height")]:r.size},..."width"in r&&Kr(r.width)&&{[n("width")]:r.width},..."height"in r&&Kr(r.height)&&{[n("height")]:r.height},[n("stroke-width")]:b,...y&&{[n("class")]:y},[n("viewBox")]:`0 0 ${i} ${o}`,...r.hasA11yProp===!1?{[n("aria-hidden")]:"true"}:{},..."attributes"in r&&r.attributes},t.node.map(N=>{const[x,c,m]=N,P=r.nonScalingStroke?{[n("vector-effect")]:"non-scaling-stroke",...c}:c;return m?[x,P,m]:[x,P]})]}/** + * @file + * @license @lucide/svelte v1.52.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const di=t=>{for(const r in t)if(r.startsWith("aria-")||r==="role"||r==="title")return!0;return!1};/** + * @file + * @license @lucide/svelte v1.52.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fi=Symbol("lucide-context"),vi=()=>No(fi);var gi=Ot("");function Ye(t,r){Me(r,!0);const a=vi()??{},n=fe(r,"color",19,()=>a.color??"currentColor"),i=fe(r,"size",19,()=>a.size??24),o=fe(r,"width",19,i),u=fe(r,"height",19,i),v=fe(r,"strokeWidth",19,()=>a.strokeWidth??2),l=fe(r,"absoluteStrokeWidth",19,()=>a.absoluteStrokeWidth??!1),y=fe(r,"nonScalingStroke",19,()=>a.nonScalingStroke??!1),b=fe(r,"iconNode",19,()=>[]),S=fe(r,"icon",19,()=>({node:b(),aliases:[],size:24})),w=We(r,["$$slots","$$events","$$legacy","color","size","width","height","strokeWidth","absoluteStrokeWidth","nonScalingStroke","iconNode","icon","class","children"]),T=z(()=>!!r.children||di(w)),N=z(()=>ui(S(),{color:n(),width:o(),height:u(),strokeWidth:v(),absoluteStrokeWidth:l(),nonScalingStroke:y(),className:en("lucide-icon",a.class),hasA11yProp:e(T),attributes:w})),x=z(()=>Tn(e(N),3)),c=z(()=>e(x)[1]),m=z(()=>Ao(e(x)[2],()=>[],!0)),P=z(()=>({...e(c),class:[...e(c).class.split(" "),r.class]}));var _=gi();In(_,()=>({...e(P)}));var R=k(_);je(R,17,()=>e(m),vn,(h,E)=>{var D=z(()=>Tn(e(E),2));let L=()=>e(D)[0],j=()=>e(D)[1];var I=Ke(),V=Y(I);Vo(V,L,!0,(O,B)=>{In(O,()=>({...j()}))}),f(h,I)});var $=A(R);ct($,()=>r.children??ba),p(_),f(t,_),Ne()}function hi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"arrow-down",size:24,node:[["path",{d:"M12 5v14"}],["path",{d:"m19 12-7 7-7-7"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function pi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"arrow-left-right",size:24,node:[["path",{d:"M8 3 4 7l4 4"}],["path",{d:"M4 7h16"}],["path",{d:"m16 21 4-4-4-4"}],["path",{d:"M20 17H4"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function mi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"arrow-up",size:24,node:[["path",{d:"m5 12 7-7 7 7"}],["path",{d:"M12 19V5"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function yn(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"chevron-down",size:24,node:[["path",{d:"m6 9 6 6 6-6"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function _i(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"chevron-right",size:24,node:[["path",{d:"m9 18 6-6-6-6"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function bi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"circle-x",size:24,node:[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],aliases:["x-circle"]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function gr(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"download",size:24,node:[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function xi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"ellipsis",size:24,node:[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}]],aliases:["more-horizontal"]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function yi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"external-link",size:24,node:[["path",{d:"M15 3h6v6"}],["path",{d:"M10 14 21 3"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function wi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"pause",size:24,node:[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function ki(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"pen-line",size:24,node:[["path",{d:"M13 21h8"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],aliases:["edit-3"]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Ci(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"pencil",size:24,node:[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Na(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"play",size:24,node:[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Si(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"plus",size:24,node:[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Yt(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"refresh-cw",size:24,node:[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Pi(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"rotate-ccw",size:24,node:[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Ri(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"settings",size:24,node:[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"}],["circle",{cx:"12",cy:"12",r:"3"}]]};Ye(t,Xe(()=>a,{get icon(){return n}}))}function Ei(t,r){let a=We(r,["$$slots","$$events","$$legacy"]);const n={name:"trash",size:24,node:[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],aliases:["trash-2"]};Ye(t,Xe(()=>a,{get icon(){return n}}))}var $i=M('
  • '),Mi=M('
      '),Ni=M('
      ',1);function st(t,r){Me(r,!0);let a=fe(r,"value",3,""),n=fe(r,"placeholder",3,"Select..."),i=fe(r,"class",3,""),o=fe(r,"buttonClass",3,""),u=F(!1),v=F(void 0),l=F(void 0),y=F(""),b=z(()=>{var L;return((L=r.options.find(j=>j.value===a()))==null?void 0:L.label)??n()});function S(L){var j;d(u,!1),(j=r.onchange)==null||j.call(r,L)}function w(){if(!e(v)||!e(l))return;const L=e(v).getBoundingClientRect(),j=e(l).offsetHeight,I=4,V=window.innerHeight-L.bottom-I,B=VV?L.top-I-j:L.bottom+I,U=Math.min(L.left,window.innerWidth-L.width);d(y,`position:fixed; top:${B}px; left:${U}px; width:${L.width}px; z-index:9999;`)}async function T(){d(u,!e(u)),e(u)&&(await St(),w())}function N(L){L.key==="Escape"&&d(u,!1)}function x(L){e(u)&&e(v)&&!e(v).contains(L.target)&&e(l)&&!e(l).contains(L.target)&&d(u,!1)}var c=Ni();Ze("mousedown",ot,x),Ze("keydown",ot,N),Ze("scroll",ot,()=>{e(u)&&d(u,!1)});var m=Y(c),P=k(m),_=k(P),R=k(_,!0);p(_);var $=A(_,2),h=k($);Le(h,{get icon(){return yn},size:14}),p($),p(P),tt(P,L=>d(v,L),()=>e(v)),p(m);var E=A(m,2);{var D=L=>{Xt(L,{children:(j,I)=>{var V=Mi();je(V,21,()=>r.options,O=>O.value,(O,B)=>{var U=$i();ye(U,"tabindex",0);var q=k(U,!0);p(U),Q(()=>{ye(U,"aria-selected",e(B).value===a()),ke(U,1,`px-2.5 py-1.5 text-[13px] cursor-pointer transition-colors duration-100 + ${e(B).value===a()?"text-edf-bleu-fonce font-bold bg-[rgba(16,87,200,0.06)]":"text-ink hover:bg-edf-gris-clair"}`),re(q,e(B).label)}),ie("mousedown",U,g=>{g.stopPropagation(),S(e(B).value)}),f(O,U)}),p(V),tt(V,O=>d(l,O),()=>e(l)),Q(()=>Ge(V,e(y))),f(j,V)}})};G(E,L=>{e(u)&&L(D)})}Q(()=>{ke(m,1,`relative inline-flex ${i()??""}`),ke(P,1,dt(o()||"flex items-center justify-between gap-1.5 w-full h-[30px] cursor-pointer bg-white border border-border rounded-md text-[13px] text-ink font-normal pl-2.5 pr-2 transition-colors duration-150 hover:bg-edf-gris-clair focus:outline-none focus:border-edf-bleu-moyen")),ye(P,"aria-expanded",e(u)),re(R,e(b)),ke($,1,`text-muted shrink-0 transition-transform duration-150 ${e(u)?"rotate-180":""}`)}),ie("mousedown",P,L=>{L.stopPropagation(),T()}),f(t,c),Ne()}Be(["mousedown"]);var Ai=Ot(''),Ti=M('
      '),Li=M('
      '),Di=M('
      ',1);function Bt(t,r){Me(r,!0);let a=fe(r,"selected",19,()=>[]),n=fe(r,"placeholder",3,"Select..."),i=fe(r,"class",3,""),o=F(!1),u=F(void 0),v=F(void 0),l=F(""),y=z(()=>new Set(a())),b=z(()=>a().length===0?n():a().length===r.options.length?`All (${r.options.length})`:a().length<=2?a().map(I=>{var V;return((V=r.options.find(O=>O.value===I))==null?void 0:V.label)??I}).join(", "):`${a().length} selected`);function S(I){var O;const V=new Set(e(y));V.has(I)?V.delete(I):V.add(I),(O=r.onchange)==null||O.call(r,[...V])}function w(){var I;(I=r.onchange)==null||I.call(r,r.options.map(V=>V.value))}function T(){var I;(I=r.onchange)==null||I.call(r,[])}function N(){if(!e(u)||!e(v))return;const I=e(u).getBoundingClientRect(),V=e(v).offsetHeight,O=4,B=window.innerHeight-I.bottom-O,q=BB?I.top-O-V:I.bottom+O,g=Math.min(I.left,window.innerWidth-I.width);d(l,`position:fixed; top:${q}px; left:${g}px; min-width:${I.width}px; z-index:9999;`)}async function x(){d(o,!e(o)),e(o)&&(await St(),N())}function c(I){e(o)&&e(u)&&!e(u).contains(I.target)&&e(v)&&!e(v).contains(I.target)&&d(o,!1)}function m(I){I.key==="Escape"&&d(o,!1)}var P=Di();Ze("mousedown",ot,c),Ze("keydown",ot,m),Ze("scroll",ot,()=>{e(o)&&d(o,!1)});var _=Y(P),R=k(_),$=k(R),h=k($,!0);p($);var E=A($,2),D=k(E);Le(D,{get icon(){return yn},size:14}),p(E),p(R),tt(R,I=>d(u,I),()=>e(u)),p(_);var L=A(_,2);{var j=I=>{Xt(I,{children:(V,O)=>{var B=Li(),U=k(B),q=k(U),g=A(q,2);p(U);var s=A(U,2);je(s,17,()=>r.options,C=>C.value,(C,H)=>{var W=Ti(),ee=k(W),ne=k(ee);{var me=le=>{var we=Ai();f(le,we)},K=z(()=>e(y).has(e(H).value));G(ne,le=>{e(K)&&le(me)})}p(ee);var X=A(ee,2),ce=k(X,!0);p(X),p(W),Q(le=>{ke(ee,1,`inline-flex items-center justify-center shrink-0 w-[14px] h-[14px] rounded border-2 transition-colors duration-100 + ${le??""}`),re(ce,e(H).label)},[()=>e(y).has(e(H).value)?"bg-edf-bleu-moyen border-edf-bleu-moyen text-white":"bg-white border-edf-gris-moyen"]),ie("mousedown",W,le=>{le.stopPropagation(),S(e(H).value)}),f(C,W)}),p(B),tt(B,C=>d(v,C),()=>e(v)),Q(()=>Ge(B,e(l))),ie("mousedown",q,C=>{C.stopPropagation(),w()}),ie("mousedown",g,C=>{C.stopPropagation(),T()}),f(V,B)}})};G(L,I=>{e(o)&&I(j)})}Q(()=>{ke(_,1,`relative inline-flex ${i()??""}`),ye(R,"aria-expanded",e(o)),re(h,e(b)),ke(E,1,`text-muted shrink-0 transition-transform duration-150 ${e(o)?"rotate-180":""}`)}),ie("mousedown",R,I=>{I.stopPropagation(),x()}),f(t,P),Ne()}Be(["mousedown"]);var Fi=M(" ",1),zi=M(''),Ii=M('Loading...'),Oi=M('No folders found'),Hi=M('
      '),ji=M('
      ',1);function Ui(t,r){Me(r,!0);const a=ws();let n=F("keep_latest"),i=F(se(a.keepLast)),o=F(se([])),u=F(se([])),v=F(!0),l=z(()=>e(n)==="keep_folder"||e(n)==="delete_folder"),y=z(()=>[{value:"keep_latest",label:"Keep latest N"},{value:"delete_all",label:"Delete all runs"},...r.cases.length===1?[{value:"keep_folder",label:"Keep specific folders"},{value:"delete_folder",label:"Delete specific folders"}]:[]]),b=z(()=>e(o).map(w=>({value:w,label:w})));un(async()=>{if(r.cases.length===1){try{d(o,await Ea(r.cases),!0)}catch{d(o,[],!0)}d(v,!1)}});async function S(){if(e(l)&&e(u).length===0){await et("Please select at least one run folder.","Missing selection");return}const w={action:e(n)};e(n)==="keep_latest"?(w.keepLast=e(i),ks({keepLast:e(i)})):e(n)==="delete_all"?w.keepLast=0:e(n)==="keep_folder"?w.keepResu=e(u):e(n)==="delete_folder"&&(w.deleteResu=e(u)),at(w)}{const w=x=>{var c=Fi(),m=Y(c);Re(m,{variant:"secondary",onclick:()=>at(null),children:(_,R)=>{Ce();var $=Ie("Cancel");f(_,$)},$$slots:{default:!0}});var P=A(m,2);Re(P,{variant:"primary",onclick:S,children:(_,R)=>{Ce();var $=Ie("Clean");f(_,$)},$$slots:{default:!0}}),f(x,c)};let T=z(()=>r.cases.length),N=z(()=>r.cases.length>1?"s":"");vr(t,{title:"Cleanup Cases",titleId:"clean-dialog-title",get subtitle(){return`${e(T)??""} case${e(N)??""} selected`},footer:w,children:(x,c)=>{var m=ji(),P=Y(m),_=k(P);$e(_,{text:"Action",children:(D,L)=>{st(D,{class:"w-[200px]",get options(){return e(y)},get value(){return e(n)},onchange:j=>d(n,j,!0)})}});var R=A(_,2);{var $=D=>{$e(D,{text:"Keep last",children:(L,j)=>{var I=zi();qe(I),Je(I,()=>e(i),V=>d(i,V)),f(L,I)}})};G(R,D=>{e(n)==="keep_latest"&&D($)})}p(P);var h=A(P,2);{var E=D=>{var L=Hi(),j=k(L);$e(j,{text:"Run folders",children:(I,V)=>{var O=Ke(),B=Y(O);{var U=s=>{var C=Ii();f(s,C)},q=s=>{var C=Oi();f(s,C)},g=s=>{Bt(s,{class:"w-full",get options(){return e(b)},get selected(){return e(u)},onchange:C=>d(u,C,!0),placeholder:"Select folders..."})};G(B,s=>{e(v)?s(U):e(o).length===0?s(q,1):s(g,-1)})}f(I,O)}}),p(L),f(D,L)};G(h,D=>{e(l)&&D(E)})}f(x,m)},$$slots:{footer:!0,default:!0}})}Ne()}function Vi(t,r){Me(r,!0);let a=z(rs);var n=Ke(),i=Y(n);{var o=y=>{$s(y,{get cases(){return e(a).props.cases}})},u=y=>{ci(y,{get cases(){return e(a).props.cases}})},v=y=>{Ui(y,{get cases(){return e(a).props.cases}})},l=y=>{hs(y,{get mode(){return e(a).type},get title(){return e(a).props.title},get message(){return e(a).props.message},get confirmLabel(){return e(a).props.confirmLabel},get confirmVariant(){return e(a).props.confirmVariant},get value(){return e(a).props.value},get placeholder(){return e(a).props.placeholder},get multiline(){return e(a).props.multiline}})};G(i,y=>{e(a).type==="run"?y(o):e(a).type==="restart"?y(u,1):e(a).type==="clean"?y(v,2):(e(a).type==="alert"||e(a).type==="confirm"||e(a).type==="prompt")&&y(l,3)})}f(t,n),Ne()}let ur=F(se([])),Bi=0;function qi(){return e(ur)}function Bn(t,r="success",a=3500){const n=++Bi;d(ur,[...e(ur),{id:n,message:t,variant:r}],!0),setTimeout(()=>Aa(n),a)}function Aa(t){d(ur,e(ur).filter(r=>r.id!==t),!0)}var Ki=M('
      '),Wi=M('
      ');function Gi(t,r){Me(r,!0);let a=z(qi);Xt(t,{children:(n,i)=>{var o=Wi();je(o,21,()=>e(a),u=>u.id,(u,v)=>{var l=Ki(),y=k(l,!0);p(l),Q(()=>{ke(l,1,`pointer-events-auto max-w-[320px] px-4 py-2.5 rounded-md shadow-lg text-sm font-semibold text-white cursor-pointer ${e(v).variant==="success"?"bg-edf-vert-fonce":"bg-edf-orange-fonce"}`),re(y,e(v).message)}),ie("click",l,()=>Aa(e(v).id)),f(u,l)}),p(o),f(n,o)}}),Ne()}Be(["click"]);var Xi=Ot(''),Yi=Ot(''),Qi=M(''),Ji=M(" ",1),Zi=M(" ",1),el=M('');function It(t,r){Me(r,!0);const a=x=>{var c=Qi(),m=k(c);{var P=R=>{var $=Xi();Q(()=>Ge($,`width: ${u()-4}px; height: ${u()-4}px;`)),f(R,$)},_=R=>{var $=Yi();Q(()=>Ge($,`width: ${u()-4}px; height: ${u()-4}px;`)),f(R,$)};G(m,R=>{n()&&!i()?R(P):i()&&R(_,1)})}p(c),Q(()=>{ke(c,1,`inline-flex items-center justify-center rounded border-2 cursor-pointer transition-colors duration-100 shrink-0 + ${o()?"opacity-40 cursor-not-allowed":""} + ${n()||i()?"bg-edf-bleu-moyen border-edf-bleu-moyen text-white":"bg-white border-edf-gris-moyen hover:border-edf-bleu-clair"}`),Ge(c,`width: ${u()??""}px; height: ${u()??""}px;`),ye(c,"aria-checked",i()?"mixed":n()),ye(c,"aria-disabled",o()),ye(c,"tabindex",o()?-1:0)}),ie("click",c,y),ie("keydown",c,b),f(x,c)};let n=fe(r,"checked",3,!1),i=fe(r,"indeterminate",3,!1),o=fe(r,"disabled",3,!1),u=fe(r,"size",3,16),v=fe(r,"label",3,""),l=fe(r,"labelFirst",3,!1);function y(x){var m;if(x.stopPropagation(),o())return;const c=i()?!0:!n();(m=r.onchange)==null||m.call(r,c)}function b(x){(x.key===" "||x.key==="Enter")&&(x.preventDefault(),x.stopPropagation(),y(x))}var S=Ke(),w=Y(S);{var T=x=>{var c=el(),m=k(c);{var P=R=>{var $=Ji(),h=Y($),E=k(h,!0);p(h);var D=A(h);a(D),Q(()=>re(E,v())),f(R,$)},_=R=>{var $=Zi(),h=Y($);a(h);var E=A(h),D=k(E,!0);p(E),Q(()=>re(D,v())),f(R,$)};G(m,R=>{l()?R(P):R(_,-1)})}p(c),ie("click",c,y),ie("keydown",c,b),f(x,c)},N=x=>{a(x)};G(w,x=>{v()?x(T):x(N,-1)})}f(t,S),Ne()}Be(["click","keydown"]);function wn(t,r){try{const a=localStorage.getItem(t);return a===null?r:JSON.parse(a)}catch{return r}}function Dr(t,r){localStorage.setItem(t,JSON.stringify(r))}const tn=new Map;function hr(t,r,a){ft(t),tn.set(t,setInterval(r,a))}function ft(t){const r=tn.get(t);r!==void 0&&(clearInterval(r),tn.delete(t))}const Dt={status:1e3,tail:1e3,plot:3e3,probe:3e3,errors:5e3},tl=["status","plot","probe","tail","errors"],Wr={status:{label:"Status table",min:500},plot:{label:"Residual plots",min:1e3},probe:{label:"Probe plots",min:1e3},tail:{label:"Log tail",min:500},errors:{label:"Recent errors",min:2e3}},ar=wn("csauto_refresh_rates",{});let mt=F(se({status:ar.status??Dt.status,tail:ar.tail??Dt.tail,plot:ar.plot??Dt.plot,probe:ar.probe??Dt.probe,errors:ar.errors??Dt.errors}));function rl(){return e(mt)}function nl(t,r){d(mt,{...e(mt),[t]:r},!0),Dr("csauto_refresh_rates",e(mt))}function al(){return e(mt).status}function qn(){return e(mt).tail}function Kn(){return e(mt).plot}function ol(){return e(mt).probe}function Wn(){return e(mt).errors}const rn=new Set;function pr(t){return rn.add(t),()=>rn.delete(t)}function sl(){rn.forEach(t=>t())}const nn=wn("csauto_autorefresh_enabled",{});function Qt(t){return nn[t]??!0}function Jt(t,r){nn[t]=r,Dr("csauto_autorefresh_enabled",nn)}var il=M(" ",1),ll=M('
      ms
      '),cl=M('
      Telemetry
      '),ul=M('
      API token
      Auto-refresh intervals
      ',1);function dl(t,r){Me(r,!0);let a=F(se(mn())),n=F(se({...rl()})),i=F(!0),o=F(!1);Ee(()=>{Xs().then(y=>{d(i,y.enabled,!0),d(o,!0)}).catch(()=>{d(o,!1)})});function u(y){const{min:b}=Wr[y];let S=e(n)[y];(typeof S!="number"||isNaN(S))&&(S=Dt[y]),d(n,{...e(n),[y]:Math.max(b,Math.round(S))},!0)}function v(y,b){const S=b.target,w=S.value.replace(/[^0-9]/g,""),T=parseInt(w,10);isNaN(T)?w===""&&d(n,{...e(n),[y]:Wr[y].min},!0):d(n,{...e(n),[y]:T},!0),S.value=String(e(n)[y])}function l(){Pa(e(a));for(const y of Object.keys(e(n)))u(y),nl(y,e(n)[y]);e(o)&&Ys(e(i)).catch(()=>{}),r.onClose()}vr(t,{title:"Settings",titleId:"settings-dialog-title",get onCancel(){return r.onClose},onConfirm:l,maxWidth:"400px",portal:!0,footer:b=>{var S=il(),w=Y(S);Re(w,{variant:"secondary",get onclick(){return r.onClose},children:(N,x)=>{Ce();var c=Ie("Cancel");f(N,c)},$$slots:{default:!0}});var T=A(w,2);Re(T,{variant:"primary",onclick:l,children:(N,x)=>{Ce();var c=Ie("Save");f(N,c)},$$slots:{default:!0}}),f(b,S)},children:(b,S)=>{var w=ul(),T=Y(w),N=A(k(T),2);qe(N),p(T);var x=A(T,2),c=k(x),m=A(k(c),2);p(c);var P=A(c,2);je(P,20,()=>tl,$=>$,($,h)=>{const E=z(()=>Wr[h]);var D=ll(),L=k(D),j=k(L),I=k(j,!0);p(j);var V=A(j,2),O=k(V);p(V),p(L);var B=A(L,2),U=k(B);qe(U),Ce(2),p(B),p(D),Q(()=>{re(I,e(E).label),re(O,`min ${e(E).min??""}ms`),gn(U,e(n)[h])}),ie("input",U,q=>v(h,q)),Ze("blur",U,()=>u(h)),f($,D)}),p(P),p(x);var _=A(x,2);{var R=$=>{var h=cl(),E=A(k(h),2);It(E,{get checked(){return e(i)},onchange:D=>d(i,D,!0),label:"Send anonymous usage statistics"}),p(h),f($,h)};G(_,$=>{e(o)&&$(R)})}Je(N,()=>e(a),$=>d(a,$)),ie("click",m,()=>d(n,{...Dt},!0)),f(b,w)},$$slots:{footer:!0,default:!0}}),Ne()}Be(["click","input"]);var fl=M(''),vl=M(' '),gl=M('
      ',1);function hl(t,r){Me(r,!0);let a=z(()=>r.shownCases!==r.totalCases),n=z(Pt),i=F(!1);var o=gl(),u=Y(o),v=k(u),l=k(v);{var y=s=>{var C=fl();Q(H=>{ye(C,"src",H),ye(C,"alt",e(n).solver)},[()=>As(e(n).solver)]),f(s,C)},b=s=>{var C=vl(),H=k(C,!0);p(C),Q(()=>re(H,e(n).solver)),f(s,C)};G(l,s=>{var C;(C=e(n))!=null&&C.logo?s(y):e(n)&&s(b,1)})}p(v);var S=A(v,2),w=k(S),T=k(w),N=k(T);{var x=s=>{var C=Ie();Q(()=>re(C,`${r.shownCases??""}/`)),f(s,C)};G(N,s=>{e(a)&&s(x)})}var c=A(N,1,!0);p(T);var m=A(T,2),P=k(m,!0);p(m),p(w);var _=A(w,2),R=k(_),$=k(R);{var h=s=>{var C=Ie();Q(()=>re(C,`${r.shownRunning??""}/`)),f(s,C)};G($,s=>{e(a)&&s(h)})}var E=A($,1,!0);p(R),Ce(2),p(_);var D=A(_,2),L=k(D),j=k(L);{var I=s=>{var C=Ie();Q(()=>re(C,`${r.shownConverged??""}/`)),f(s,C)};G(j,s=>{e(a)&&s(I)})}var V=A(j,1,!0);p(L),Ce(2),p(D),p(S);var O=A(S,2),B=k(O),U=k(B);Le(U,{get icon(){return Ri},size:18}),p(B),p(O),p(u);var q=A(u,2);{var g=s=>{dl(s,{onClose:()=>d(i,!1)})};G(q,s=>{e(i)&&s(g)})}Q(()=>{re(c,r.totalCases),re(P,r.totalCases===1?"case":"cases"),re(E,r.totalRunning),re(V,r.totalConverged)}),ie("click",B,()=>d(i,!0)),f(t,o),Ne()}Be(["click"]);const pl=""+new URL("../assets/simvia-logo.kLDq7Uoj.svg",import.meta.url).href;var ml=M('
      '),_l=M('

      '),bl=M("
      "),xl=M('
      '),yl=M('
      '),wl=M('
      ');function Ht(t,r){let a=fe(r,"eyebrow",3,""),n=fe(r,"title",3,""),i=fe(r,"wide",3,!1);var o=wl(),u=A(k(o),2);{var v=y=>{var b=yl(),S=k(b);{var w=c=>{var m=Ke(),P=Y(m);ct(P,()=>r.titleSlot),f(c,m)},T=c=>{var m=bl(),P=k(m);{var _=h=>{var E=ml(),D=k(E,!0);p(E),Q(()=>re(D,a())),f(h,E)};G(P,h=>{a()&&h(_)})}var R=A(P,2);{var $=h=>{var E=_l(),D=k(E,!0);p(E),Q(()=>re(D,n())),f(h,E)};G(R,h=>{n()&&h($)})}p(m),f(c,m)};G(S,c=>{r.titleSlot?c(w):c(T,-1)})}var N=A(S,2);{var x=c=>{var m=xl(),P=k(m);ct(P,()=>r.actions),p(m),f(c,m)};G(N,c=>{r.actions&&c(x)})}p(b),f(y,b)};G(u,y=>{(a()||n()||r.titleSlot||r.actions)&&y(v)})}var l=A(u,2);ct(l,()=>r.children),p(o),Q(()=>{ke(o,1,`${i()?"col-span-12":"col-span-6"} bg-card border border-border rounded-[10px] p-[16px_18px_18px] relative overflow-clip animate-rise`),ye(o,"id",r.id)}),f(t,o)}function Zt(t,r){Me(r,!0);let a=fe(r,"checked",15,!0);Ee(()=>{a()?hr(r.name,r.onRefresh,r.intervalMs):ft(r.name)}),dr(()=>ft(r.name)),It(t,{get checked(){return a()},onchange:n=>a(n),size:14,label:"Auto-refresh",labelFirst:!0}),Ne()}var kl=M(" ",1),Cl=M('
    • '),Sl=M(''),Pl=M(' ',1);function Rl(t,r){Me(r,!0);let a=fe(r,"label",3,"More"),n=F(!1),i=F(void 0),o=F(void 0),u=F("");function v(){if(!e(i)||!e(o))return;const P=e(i).getBoundingClientRect(),_=e(o).offsetHeight,R=4,$=window.innerHeight-P.bottom-R,E=$<_&&P.top-R>$?P.top-R-_:P.bottom+R,D=Math.min(P.left,window.innerWidth-180);d(u,`position:fixed; top:${E}px; left:${D}px; min-width:170px; z-index:9999;`)}async function l(){var P,_;d(n,!e(n)),e(n)&&(await St(),v(),(_=(P=e(o))==null?void 0:P.querySelector("button:not(:disabled)"))==null||_.focus({preventScroll:!0}))}function y(){var P,_;d(n,!1),(_=(P=e(i))==null?void 0:P.querySelector("button"))==null||_.focus()}function b(P){P.disabled||(y(),P.onClick())}function S(P){var h,E;if(P.key!=="ArrowDown"&&P.key!=="ArrowUp")return;P.preventDefault();const _=Array.from(((h=e(o))==null?void 0:h.querySelectorAll("button:not(:disabled)"))??[]),R=_.indexOf(document.activeElement),$=P.key==="ArrowDown"?1:_.length-1;(E=_[(R+$)%_.length])==null||E.focus()}function w(P){e(n)&&e(i)&&!e(i).contains(P.target)&&e(o)&&!e(o).contains(P.target)&&d(n,!1)}var T=Pl();Ze("mousedown",ot,w),Ze("keydown",ot,P=>{P.key==="Escape"&&e(n)&&y()}),Ze("scroll",ot,()=>{e(n)&&d(n,!1)});var N=Y(T),x=k(N);Re(x,{variant:"secondary",size:"sm",onclick:l,children:(P,_)=>{var R=kl(),$=Y(R);Le($,{get icon(){return xi}});var h=A($);Q(()=>re(h,` ${a()??""}`)),f(P,R)},$$slots:{default:!0}}),p(N),tt(N,P=>d(i,P),()=>e(i));var c=A(N,2);{var m=P=>{Xt(P,{children:(_,R)=>{var $=Sl();je($,21,()=>r.items,h=>h.label,(h,E)=>{var D=Cl(),L=k(D),j=k(L,!0);p(L),p(D),Q(()=>{L.disabled=e(E).disabled,ye(L,"aria-disabled",e(E).disabled),ke(L,1,`w-full flex items-center gap-2 text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] text-ink transition-[background] duration-[120ms] ease-in-out disabled:opacity-40 disabled:pointer-events-none ${e(E).disabled?"":"cursor-pointer hover:bg-edf-gris-clair hover:border-border"}`),re(j,e(E).label)}),ie("click",L,()=>b(e(E))),f(h,D)}),p($),tt($,h=>d(o,h),()=>e(o)),Q(()=>Ge($,e(u))),ie("keydown",$,S),f(_,$)}})};G(c,P=>{e(n)&&P(m)})}f(t,T),Ne()}Be(["keydown","click"]);const Ft=[{key:"note",label:"Note"},{key:"nprocs",label:"MPI Ranks"},{key:"nt",label:"Thread Count"},{key:"last_iter",label:"Last Iter"},{key:"duration",label:"Duration"},{key:"last_mod",label:"Last Modified"},{key:"resu_size_mb",label:"Results (MB)"}];let qt=F(se([])),$r=F(se([])),gt=F(se(new Set)),mr=F(""),kn=F(""),Rt=F(se([])),nt=F(se([])),Kt=F(""),Ta=F(!0),El=F(!0),Vt=F(se(wn("csauto_status_views",{}))),Cn=F(se(localStorage.getItem("csauto_status_view_selected")??""));function Fr(){return e(qt)}function Sn(){return e($r)}function jt(){return e(gt)}function Gn(){return e(mr)}function an(){return e(kn)}function $l(){return e(Rt)}function Ml(){return e(Rt).filter(t=>e($r).includes(t))}function Nl(){return e(Rt).filter(t=>Ft.some(r=>r.key===t))}function Al(){return e(nt)}function Tl(){return e(Kt)}function Ll(){return e(Ta)}function Xn(){return e(Vt)}function Dl(){return e(Cn)}function Fl(t){d(qt,t,!0)}function zl(t){const r=e($r).length===0&&t.length>0;d($r,t,!0),r&&e(Rt).length===0&&d(Rt,[...t,...Ft.map(a=>a.key)],!0)}function Il(t){d(mr,t,!0)}function Yn(t){d(Rt,t,!0)}function Ol(t){d(Kt,t,!0)}function Hl(t){d(Ta,t,!0)}function jl(t){d(El,t,!0)}function Pn(t){d(Cn,t,!0),localStorage.setItem("csauto_status_view_selected",t)}function on(t){const r=new Set(e(gt));r.has(t)?r.delete(t):r.add(t),d(gt,r,!0),d(mr,t,!0),d(kn,t,!0)}function La(t){d(gt,new Set([t]),!0),d(mr,t,!0),d(kn,t,!0)}function sn(t,r,a){const n=a.indexOf(t),i=a.indexOf(r);if(n<0||i<0)return;const[o,u]=n{const a=Ul(r);return t.every(n=>a.includes(n))})}function Vl(t){if(t==null||t==="")return"";const r=Number(t);return Number.isFinite(r)?r:String(t).toLowerCase()}function Zn(t,r){var n;if(r==="case_id")return t.case_id;if(r==="status")return t.status??"";if(r==="note")return t.note??"";if(r==="nprocs")return t.nprocs??0;if(r==="nt")return t.nt??0;if(r==="last_iter")return t.last_iter??0;if(r==="duration")return t.duration_s??0;if(r==="last_mod")return t.last_mod??"";if(r==="resu_size_mb")return t.resu_size_mb??0;const a=(n=t.doe)==null?void 0:n[r];return a!==void 0?Vl(a):""}function Bl(t,r){return t===""&&r===""?0:t===""?1:r===""?-1:typeof t=="number"&&typeof r=="number"?t-r:String(t).localeCompare(String(r))}function Fa(){const t=Da();return e(nt).length===0?t:[...t].sort((r,a)=>{for(const n of e(nt)){const i=Zn(r,n.key),o=Zn(a,n.key),u=Bl(i,o);if(u!==0)return n.dir==="asc"?u:-u}return 0})}function Pr(){return Fa().map(t=>t.case_id)}function ql(t,r){const a=e(nt).findIndex(n=>n.key===t);if(r)if(a>=0){const n=[...e(nt)];n[a]={key:t,dir:n[a].dir==="asc"?"desc":"asc"},d(nt,n,!0)}else d(nt,[...e(nt),{key:t,dir:"asc"}],!0);else a>=0&&e(nt).length===1?d(nt,[{key:t,dir:e(nt)[0].dir==="asc"?"desc":"asc"}],!0):d(nt,[{key:t,dir:"asc"}],!0)}function Kl(){return{visibleColumns:[...e(Rt)],search:e(Kt),sorts:e(nt).map(t=>({...t}))}}function Wl(t){d(Rt,[...t.visibleColumns],!0),d(Kt,t.search,!0),d(nt,t.sorts.map(r=>({...r})),!0)}function Gl(t){d(Vt,{...e(Vt),[t]:Kl()},!0),Dr("csauto_status_views",e(Vt)),Pn(t)}function Xl(t){const r={...e(Vt)};delete r[t],d(Vt,r,!0),Dr("csauto_status_views",r),e(Cn)===t&&Pn("")}function wr(){const t=new Set;for(const r of e(qt))e(gt).has(r.case_id)&&t.add((r.status??"").toUpperCase());return t}function ea(){for(const t of e(qt))if(e(gt).has(t.case_id)&&t.resu_size_mb!=null&&t.resu_size_mb>0)return!0;return!1}function Yl(t,r){const a=r.length,n=t.length,i=r.filter(l=>l.status==="RUNNING").length,o=t.filter(l=>l.status==="RUNNING").length,u=r.filter(l=>l.convergence==="converged").length,v=t.filter(l=>l.convergence==="converged").length;return{totalCases:a,shownCases:n,totalRunning:i,shownRunning:o,totalConverged:u,shownConverged:v}}var Ql=M('
      DOE columns
      '),Jl=M('
      ');function Zl(t,r){Me(r,!0);let a=fe(r,"initialName",3,""),n=z(Sn),i=F(se(a())),o=F(se(new Set(r.initialColumns??[...e(n),...Ft.map(O=>O.key)])));function u(O){const B=new Set(e(o));B.has(O)?B.delete(O):B.add(O),d(o,B,!0)}function v(O,B){const U=new Set(e(o));O.forEach(q=>B?U.add(q):U.delete(q)),d(o,U,!0)}function l(){const O=e(i).trim();O&&r.onSave(O,[...e(o)])}function y(O){O.key==="Escape"&&r.onCancel()}function b(O){O.target.dataset.backdrop!==void 0&&r.onCancel()}let S=F(void 0);Ee(()=>{var O,B;(O=e(S))==null||O.focus(),(B=e(S))==null||B.select()});var w=Jl(),T=k(w),N=k(T),x=k(N,!0);p(N);var c=A(N,2),m=A(k(c),2);qe(m),tt(m,O=>d(S,O),()=>e(S)),p(c);var P=A(c,2);{var _=O=>{var B=Ql(),U=k(B),q=A(k(U),2),g=k(q),s=A(g,2);p(q),p(U);var C=A(U,2);je(C,20,()=>e(n),H=>H,(H,W)=>{{let ee=z(()=>e(o).has(W));It(H,{get checked(){return e(ee)},onchange:()=>u(W),size:14,get label(){return W}})}}),p(C),p(B),ie("click",g,()=>v(e(n),!0)),ie("click",s,()=>v(e(n),!1)),f(O,B)};G(P,O=>{e(n).length>0&&O(_)})}var R=A(P,2),$=k(R),h=A(k($),2),E=k(h),D=A(E,2);p(h),p($);var L=A($,2);je(L,21,()=>Ft,O=>O.key,(O,B)=>{{let U=z(()=>e(o).has(e(B).key));It(O,{get checked(){return e(U)},onchange:()=>u(e(B).key),size:14,get label(){return e(B).label}})}}),p(L),p(R);var j=A(R,2),I=k(j);Re(I,{variant:"secondary",get onclick(){return r.onCancel},children:(O,B)=>{Ce();var U=Ie("Cancel");f(O,U)},$$slots:{default:!0}});var V=A(I,2);{let O=z(()=>!e(i).trim());Re(V,{variant:"primary",onclick:l,get disabled(){return e(O)},children:(B,U)=>{Ce();var q=Ie("Save");f(B,q)},$$slots:{default:!0}})}p(j),p(T),p(w),Q(()=>re(x,r.mode==="create"?"New view":"Edit view")),ie("keydown",w,y),ie("click",w,b),Je(m,()=>e(i),O=>d(i,O)),ie("click",E,()=>v(Ft.map(O=>O.key),!0)),ie("click",D,()=>v(Ft.map(O=>O.key),!1)),f(t,w),Ne()}Be(["keydown","click"]);var ec=M('
      ',1);function tc(t,r){Me(r,!0);let a=z(()=>Object.keys(Xn())),n=F(se(Dl())),i=F(!1),o=F("create"),u=z(()=>[{value:"",label:"All columns"},...e(a).map($=>({value:$,label:$}))]);function v($){if(d(n,$,!0),Pn($),$==="")Yn([...Sn(),...Ft.map(h=>h.key)]);else{const h=Xn();h[$]&&Wl(h[$])}}function l(){d(o,"edit"),d(i,!0)}function y(){d(o,"create"),d(i,!0)}function b($,h){Yn(h),e(o)==="edit"&&e(n)&&$!==e(n)&&Xl(e(n)),Gl($),d(n,$,!0),d(i,!1)}function S(){d(i,!1)}var w=ec(),T=Y(w),N=k(T);st(N,{class:"w-[140px]",get options(){return e(u)},get value(){return e(n)},onchange:v});var x=A(N,2),c=k(x);Le(c,{get icon(){return Ci},size:14}),p(x);var m=A(x,2),P=k(m);Le(P,{get icon(){return Si},size:14}),p(m),p(T);var _=A(T,2);{var R=$=>{Xt($,{children:(h,E)=>{{let D=z(()=>e(o)==="edit"?e(n):""),L=z($l);Zl(h,{get mode(){return e(o)},get initialName(){return e(D)},get initialColumns(){return e(L)},onSave:b,onCancel:S})}}})};G(_,$=>{e(i)&&$(R)})}ie("click",x,l),ie("click",m,y),f(t,w),Ne()}Be(["click"]);var rc=M('
      '),nc=M(""),ac=M(' '),oc=M(' '),sc=M('
      '),ic=M(" "),lc=M(''),cc=M(''),uc=M(" Open GUI",1),dc=M(""),fc=M(" "),vc=M(""),gc=M('
      ');function hc(t,r){Me(r,!0);let a=z(()=>r.rows.length>0&&r.rows.every(g=>e(y).has(g.case_id))),n=z(()=>r.rows.some(g=>e(y).has(g.case_id))),i=z(()=>e(n)&&!e(a)),o=z(Ml),u=z(()=>new Set(Nl())),v=z(Al),l=z(Ll),y=z(jt),b=z(()=>{const g=[{key:"_select",label:"",kind:"meta",sticky:"left"},{key:"case_id",label:"Case",kind:"meta",sticky:"left"},{key:"status",label:"Status",kind:"meta",sticky:"left"},...e(u).has("note")?[{key:"note",label:"Note",kind:"meta"}]:[]],s=e(o).map(ee=>({key:ee,label:ee,kind:"doe"})),H=[{key:"nprocs",label:"MPI Ranks",kind:"calc"},{key:"nt",label:"Thread Count",kind:"calc"},{key:"last_iter",label:"Last Iter",kind:"calc"},{key:"duration",label:"Duration",kind:"calc"},{key:"last_mod",label:"Last Modified",kind:"calc"},{key:"resu_size_mb",label:"Results (MB)",kind:"calc"}].filter(ee=>e(u).has(ee.key)),W=Zr("gui")?[{key:"_actions",label:"",kind:"meta",sticky:"right"}]:[];return[...g,...s,...H,...W]});function S(g){const s=e(v).findIndex(H=>H.key===g);if(s<0)return"";const C=e(v)[s].dir==="asc"?"▲":"▼";return e(v).length>1?`${C}${s+1}`:C}function w(g,s){g!=="_actions"&&ql(g,s.ctrlKey||s.metaKey)}function T(g,s){s.shiftKey?sn(an()||g,g,Pr()):s.ctrlKey||s.metaKey?on(g):La(g)}function N(g,s){var H;const C=((H=g.status)==null?void 0:H.toUpperCase())??"";C!=="DONE"&&C!=="FAILED"||(s.preventDefault(),r.onContextMenu(g.case_id,s.clientX,s.clientY,g.convergence??""))}async function x(g){const s=g.note??"",C=await pn("Case note:",s,`Note: ${g.case_id}`,"",!0);if(C!==null)try{await Ks(g.case_id,C)}catch(H){await et(`Failed to set note: ${H instanceof Error?H.message:H}`,"Error")}}async function c(g){try{await Gs(g)}catch(s){await et(`Failed to open GUI: ${s instanceof Error?s.message:s}`,"Error")}}function m(g,s){const C=(g==null?void 0:g.toUpperCase())??"";return(C==="DONE"||C==="FAILED")&&s==="converged"?"CONVERGED":(C==="DONE"||C==="FAILED")&&s==="not_converged"?"NOT CONVERGED":C}function P(g,s){const C=(g==null?void 0:g.toUpperCase())??"";return(C==="DONE"||C==="FAILED")&&s==="converged"?"status-converged":(C==="DONE"||C==="FAILED")&&s==="not_converged"?"status-not-converged":C==="RUNNING"?"status-running":C==="DONE"?"status-done":C==="FAILED"?"status-failed":C==="PREPARED"?"status-prepared":"status-unknown"}function _(g){return e(y).has(g.case_id)?"row-selected":""}function R(g,s){var C;return s.key==="case_id"?g.case_id:s.key==="nprocs"?g.nprocs!=null?String(g.nprocs):"":s.key==="nt"?g.nt!=null?String(g.nt):"":s.key==="last_iter"?g.last_iter!=null?String(g.last_iter):"":s.key==="duration"?g.duration??"":s.key==="last_mod"?$(g.last_mod):s.key==="resu_size_mb"?g.resu_size_mb!=null?String(g.resu_size_mb):"":s.kind==="doe"?((C=g.doe)==null?void 0:C[s.key])!=null&&String(g.doe[s.key])!==""?String(g.doe[s.key]):"-":""}function $(g){if(!g)return"";try{const s=new Date(g),C=new Date,H=s.toDateString()===C.toDateString(),W=new Date(C);W.setDate(W.getDate()-1);const ee=s.toDateString()===W.toDateString(),ne=s.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});return H?`Today ${ne}`:ee?`Yesterday ${ne}`:s.toLocaleDateString([],{month:"short",day:"numeric"})+` ${ne}`}catch{return g}}let h=F(se({})),E=F(void 0);Ee(()=>{e(l),e(b),r.rows,!(!e(E)||!e(l))&&St().then(()=>{if(!e(E))return;const g=Array.from(e(E).querySelectorAll("thead th.sticky-left")),s={};let C=0;g.forEach(H=>{const W=H.dataset.colKey??"";s[W]=C,C+=H.getBoundingClientRect().width}),d(h,s,!0)})});function D(g,s,C){const H=C&&g.key!=="_select"&&g.key!=="_actions"?["status-sortable"]:[];if(!C&&g.key==="case_id"&&H.push("case-id"),g.kind==="doe"&&H.push("status-col-doe"),!C&&g.key==="note"&&H.push("max-w-[140px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer hover:underline hover:decoration-dotted hover:underline-offset-2"),e(l)&&g.sticky==="left"&&H.push("sticky-left"),e(l)&&g.sticky==="right"&&H.push("sticky-right"),e(l)&&g.sticky==="left"){const W=e(b)[s+1];(!W||W.sticky!=="left")&&H.push("sticky-divider")}if(e(l)&&g.sticky==="right"){const W=e(b)[s-1];(!W||W.sticky!=="right")&&H.push("sticky-divider")}return H.join(" ")}function L(g,s){return D(g,s,!0)}function j(g,s){return D(g,s,!1)}function I(g){return!e(l)||!g.sticky?"":g.sticky==="right"?"position:sticky;right:0;":g.sticky==="left"&&g.key in e(h)?`position:sticky;left:${e(h)[g.key]}px;`:""}var V=gc(),O=k(V),B=k(O),U=k(B);je(U,23,()=>e(b),g=>g.key,(g,s,C)=>{var H=Ke(),W=Y(H);{var ee=K=>{var X=rc(),ce=k(X),le=k(ce);It(le,{get checked(){return e(a)},get indeterminate(){return e(i)},get onchange(){return r.onToggleAll},size:14}),p(ce),p(X),Q((we,he)=>{ke(X,1,`${we??""} !px-2.5 !py-0 w-9`),ye(X,"data-col-key",e(s).key),Ge(X,he)},[()=>L(e(s),e(C)),()=>I(e(s))]),f(K,X)},ne=K=>{var X=nc();Q((ce,le)=>{ke(X,1,ce),ye(X,"data-col-key",e(s).key),Ge(X,le)},[()=>dt(L(e(s),e(C))),()=>I(e(s))]),f(K,X)},me=K=>{var X=oc(),ce=k(X),le=A(ce);{var we=Z=>{var _e=ac(),Se=k(_e,!0);p(_e),Q(Pe=>re(Se,Pe),[()=>S(e(s).key)]),f(Z,_e)},he=z(()=>S(e(s).key));G(le,Z=>{e(he)&&Z(we)})}p(X),Q((Z,_e)=>{ke(X,1,Z),ye(X,"data-col-key",e(s).key),Ge(X,_e),re(ce,`${e(s).label??""} `)},[()=>dt(L(e(s),e(C))),()=>I(e(s))]),ie("click",X,Z=>w(e(s).key,Z)),ie("keydown",X,Z=>{(Z.key==="Enter"||Z.key===" ")&&(Z.preventDefault(),w(e(s).key,Z))}),f(K,X)};G(W,K=>{e(s).key==="_select"?K(ee):e(s).key==="_actions"?K(ne,1):K(me,-1)})}f(g,H)}),p(U),p(B);var q=A(B);je(q,21,()=>r.rows,g=>g.case_id,(g,s)=>{var C=vc();je(C,23,()=>e(b),H=>H.key,(H,W,ee)=>{var ne=Ke(),me=Y(ne);{var K=he=>{var Z=sc(),_e=k(Z),Se=k(_e);{let Pe=z(()=>e(y).has(e(s).case_id));It(Se,{get checked(){return e(Pe)},onchange:()=>r.onToggleRow(e(s).case_id),size:14})}p(_e),p(Z),Q((Pe,ge)=>{ke(Z,1,`${Pe??""} !px-2.5 !py-0 w-9`),ye(Z,"data-col-key",e(W).key),Ge(Z,ge)},[()=>j(e(W),e(ee)),()=>I(e(W))]),f(he,Z)},X=he=>{var Z=ic(),_e=k(Z),Se=k(_e,!0);p(_e),p(Z),Q((Pe,ge,De,be)=>{ke(Z,1,Pe),ye(Z,"data-col-key",e(W).key),Ge(Z,ge),ke(_e,1,`status-pill ${De??""}`),re(Se,be)},[()=>dt(j(e(W),e(ee))),()=>I(e(W)),()=>P(e(s).status,e(s).convergence),()=>m(e(s).status,e(s).convergence)]),f(he,Z)},ce=he=>{var Z=cc(),_e=k(Z);{var Se=ge=>{var De=Ie();Q(()=>re(De,e(s).note)),f(ge,De)},Pe=ge=>{var De=lc(),be=k(De);Le(be,{get icon(){return ki},size:12}),p(De),f(ge,De)};G(_e,ge=>{e(s).note?ge(Se):ge(Pe,-1)})}p(Z),Q((ge,De)=>{ke(Z,1,ge),ye(Z,"data-col-key",e(W).key),Ge(Z,De),ye(Z,"title",e(s).note||"Add note")},[()=>dt(j(e(W),e(ee))),()=>I(e(W))]),ie("click",Z,ge=>{ge.stopPropagation(),x(e(s))}),ie("keydown",Z,ge=>{(ge.key==="Enter"||ge.key===" ")&&(ge.preventDefault(),ge.stopPropagation(),x(e(s)))}),f(he,Z)},le=he=>{var Z=dc(),_e=k(Z);Re(_e,{variant:"primary",size:"sm",onclick:Se=>{Se.stopPropagation(),c(e(s).case_id)},children:(Se,Pe)=>{var ge=uc(),De=Y(ge);Le(De,{get icon(){return yi},size:12}),Ce(),f(Se,ge)},$$slots:{default:!0}}),p(Z),Q((Se,Pe)=>{ke(Z,1,Se),ye(Z,"data-col-key",e(W).key),Ge(Z,Pe)},[()=>dt(j(e(W),e(ee))),()=>I(e(W))]),f(he,Z)},we=he=>{var Z=fc(),_e=k(Z,!0);p(Z),Q((Se,Pe,ge)=>{ke(Z,1,Se),ye(Z,"data-col-key",e(W).key),Ge(Z,Pe),re(_e,ge)},[()=>dt(j(e(W),e(ee))),()=>I(e(W)),()=>R(e(s),e(W))]),f(he,Z)};G(me,he=>{e(W).key==="_select"?he(K):e(W).key==="status"?he(X,1):e(W).key==="note"?he(ce,2):e(W).key==="_actions"?he(le,3):he(we,-1)})}f(H,ne)}),p(C),Q(H=>ke(C,1,H),[()=>dt(_(e(s)))]),ie("click",C,H=>T(e(s).case_id,H)),ie("contextmenu",C,H=>N(e(s),H)),f(g,C)}),p(q),p(O),tt(O,g=>d(E,g),()=>e(E)),p(V),Q(()=>ke(V,1,`table-wrap ${e(l)?"sticky-enabled":""}`)),f(t,V),Ne()}Be(["click","keydown","contextmenu"]);var pc=M('
      ');function mc(t,r){Me(r,!0);let a=z(()=>{const w=Math.min(r.x,window.innerWidth-180),T=Math.min(r.y,window.innerHeight-120);return`left: ${w}px; top: ${T}px;`});async function n(w){try{for(const T of r.cases)await Ws(T,w)}catch(T){console.error("Failed to set convergence:",T)}r.onClose()}const i=r.cases.length>1?` (${r.cases.length})`:"";var o=pc();Ze("keydown",ot,w=>{w.key==="Escape"&&r.onClose()});var u=k(o),v=k(u);p(u);var l=A(u,2),y=k(l);p(l);var b=A(l,2),S=k(b);p(b),p(o),Q(()=>{Ge(o,e(a)),ke(u,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue==="converged"?"is-active":""}`),re(v,`Mark Converged${i}`),ke(l,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue==="not_converged"?"is-active":""}`),re(y,`Mark Not Converged${i}`),ke(b,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue===""?"is-active":""}`),re(S,`Clear Mark${i}`)}),ie("click",u,()=>n("converged")),ie("click",l,()=>n("not_converged")),ie("click",b,()=>n("")),f(t,o),Ne()}Be(["click"]);var _c=M(" Refresh",1),bc=M(" ",1),xc=M(" Run",1),yc=M(" Restart",1),wc=M(" Kill",1),kc=M(" Clean",1),Cc=M('
      ',1),Sc=M('
      ',1);function Pc(t,r){Me(r,!0);function a(){return[...jt()]}async function n(){const g=a();if(!g.length)return;const s=await ns(g);if(s)try{await Un({cases:g,n:s.n,nt:s.nt,maxParallel:s.maxParallel}),r.onRefresh()}catch(C){await et(`Run failed: ${C instanceof Error?C.message:C}`,"Error")}}async function i(){const g=a();if(!g.length)return;const s=await as(g);if(s)try{await Un({cases:g,...s,restart:!0}),r.onRefresh()}catch(C){await et(`Restart failed: ${C instanceof Error?C.message:C}`,"Error")}}async function o(){const g=a();if(!(!g.length||!await Hn(`Kill ${g.length} case${g.length>1?"s":""}?`,"Confirm Kill","Kill","danger")))try{await Vs(g),r.onRefresh()}catch(C){await et(`Kill failed: ${C instanceof Error?C.message:C}`,"Error")}}async function u(g){const s=new Set(Fr().filter(ee=>{var ne;return((ne=ee.status)==null?void 0:ne.toUpperCase())==="RUNNING"}).map(ee=>ee.case_id)),C=a().filter(ee=>s.has(ee));if(!C.length)return;const H=`${C.length} case${C.length>1?"s":""}`;let W=null;if(g.value_label){const ee=await pn(`${g.value_label} for ${H}:`,"",g.label);if(ee===null)return;W=Number(ee.trim().replace(",","."));const ne=Ma(g,W);if(ne){await et(ne,"Invalid value");return}}else if(!await Hn(`${g.label} for ${H}?`,g.label,g.label,"warning"))return;try{await Bs({cases:C,action:g.name,value:W}),r.onRefresh(),Bn(`${g.label} requested for ${H}`)}catch(ee){await et(`${g.label} failed: ${ee instanceof Error?ee.message:ee}`,"Error")}}async function v(){var C;const g=a();if(!g.length)return;const s=await os(g);if(s)try{const H=await qs({cases:g,keepLast:s.keepLast,keepResu:s.keepResu,deleteResu:s.deleteResu,pruneResu:!0});(C=H.skipped_active)!=null&&C.length&&Bn(`Skipped running cases: ${H.skipped_active.join(", ")}`),r.onRefresh(),sl()}catch(H){await et(`Cleanup failed: ${H instanceof Error?H.message:H}`,"Error")}}function l(){const g=Pr();g.length>0&&g.every(C=>jt().has(C))?Jn():Qn(g)}function y(g){on(g)}let b=F(se(Qt("status")));Ee(()=>{Jt("status",e(b))}),Hl(!0);let S=z(()=>{const g=wr();return g.size>0&&[...g].some(s=>s==="PREPARED"||s==="DONE"||s==="FAILED")}),w=z(()=>{if(!Zr("restart"))return!1;const g=wr();return g.size>0&&[...g].some(s=>s==="DONE"||s==="FAILED")&&ea()}),T=z(()=>{const g=wr();return g.size>0&&g.has("RUNNING")}),N=z(()=>wr().has("RUNNING")),x=z(()=>{var g;return(((g=Pt())==null?void 0:g.control_actions)??[]).map(s=>({label:s.label,onClick:()=>u(s),disabled:!e(N)}))}),c=z(ea),m=F(se(Tl())),P=null;function _(){P&&clearTimeout(P),P=setTimeout(()=>{Ol(e(m))},200)}Ee(()=>jl(e(b)));let R=F(!1),$=F(0),h=F(0),E=F(se([])),D=F("");function L(g,s,C,H){d(E,jt().has(g)?[...jt()]:[g],!0),d(D,H,!0),d($,s,!0),d(h,C,!0),d(R,!0)}function j(){d(R,!1)}function I(g){const s=Pr();if(s.length){if(g.key==="Escape"){Jn(),j();return}if((g.ctrlKey||g.metaKey)&&g.key==="a"){g.preventDefault(),Qn(s);return}if(g.key==="ArrowDown"||g.key==="ArrowUp"){g.preventDefault();const C=g.key==="ArrowDown"?1:-1,H=Gn(),W=s.indexOf(H),ee=Math.max(0,Math.min(s.length-1,W+C)),ne=s[ee];g.shiftKey?sn(an()||s[0],ne,s):La(ne),Il(ne);return}if(g.key===" "){g.preventDefault();const C=Gn();C&&(g.shiftKey?sn(an()||C,C,Pr()):on(C))}}}var V=Sc();Ze("click",ot,j),Ze("scroll",ot,j);var O=Y(V),B=k(O);Ht(B,{eyebrow:"Overview",title:"Status",wide:!0,id:"status-card-shell",actions:s=>{var C=bc(),H=Y(C);{let ne=z(al);Zt(H,{name:"status",get intervalMs(){return e(ne)},get onRefresh(){return r.onRefresh},get checked(){return e(b)},set checked(me){d(b,me,!0)}})}var W=A(H,2);{var ee=ne=>{Re(ne,{variant:"primary",onclick:()=>r.onRefresh(),children:(me,K)=>{var X=_c(),ce=Y(X);Le(ce,{get icon(){return Yt}}),Ce(),f(me,X)},$$slots:{default:!0}})};G(W,ne=>{e(b)||ne(ee)})}f(s,C)},children:(s,C)=>{var H=Cc(),W=Y(H),ee=k(W),ne=k(ee);qe(ne);var me=A(ne,2);tc(me,{}),p(ee);var K=A(ee,2),X=k(K),ce=k(X);p(X);var le=A(X,2);{let be=z(()=>!e(S));Re(le,{variant:"run",size:"sm",onclick:n,get disabled(){return e(be)},children:(Oe,Fe)=>{var oe=xc(),ue=Y(oe);Le(ue,{get icon(){return Na}}),Ce(),f(Oe,oe)},$$slots:{default:!0}})}var we=A(le,2);{var he=be=>{{let Oe=z(()=>!e(w));Re(be,{variant:"warning",size:"sm",onclick:i,get disabled(){return e(Oe)},children:(Fe,oe)=>{var ue=yc(),He=Y(ue);Le(He,{get icon(){return Pi}}),Ce(),f(Fe,ue)},$$slots:{default:!0}})}},Z=z(()=>Zr("restart"));G(we,be=>{e(Z)&&be(he)})}var _e=A(we,2);{var Se=be=>{Rl(be,{label:"Control",get items(){return e(x)}})};G(_e,be=>{e(x).length>0&&be(Se)})}var Pe=A(_e,2);{let be=z(()=>!e(T));Re(Pe,{variant:"danger",size:"sm",onclick:o,get disabled(){return e(be)},children:(Oe,Fe)=>{var oe=wc(),ue=Y(oe);Le(ue,{get icon(){return bi}}),Ce(),f(Oe,oe)},$$slots:{default:!0}})}var ge=A(Pe,2);{let be=z(()=>!e(c));Re(ge,{variant:"secondary",size:"sm",onclick:v,get disabled(){return e(be)},children:(Oe,Fe)=>{var oe=kc(),ue=Y(oe);Le(ue,{get icon(){return Ei}}),Ce(),f(Oe,oe)},$$slots:{default:!0}})}p(K),p(W);var De=A(W,2);{let be=z(Fa);hc(De,{get rows(){return e(be)},onContextMenu:L,onToggleAll:l,onToggleRow:y})}Q(be=>re(ce,`${be??""} selected`),[()=>jt().size]),ie("input",ne,_),Je(ne,()=>e(m),be=>d(m,be)),f(s,H)},$$slots:{actions:!0,default:!0}}),p(O);var U=A(O,2);{var q=g=>{mc(g,{get cases(){return e(E)},get x(){return e($)},get y(){return e(h)},get currentValue(){return e(D)},onClose:j})};G(U,g=>{e(R)&&g(q)})}ie("keydown",O,I),f(t,V),Ne()}Be(["keydown","input"]);var Rc=M('
      ');function Wt(t,r){var a=Rc(),n=k(a);ct(n,()=>r.children),p(a),f(t,a)}var Ec=M(''),$c=M(" ",1);function Rn(t,r){Me(r,!0);let a=fe(r,"columnLabel",3,"Columns"),n=fe(r,"xMinLabel",3,"Iter min"),i=z(()=>xn(r.allCases)),o=z(()=>r.columns.map(l=>({value:l,label:l}))),u=[{value:"zero",label:"Zero"},{value:"restart",label:"Restart start"},{value:"custom",label:"Custom"}];function v(l){r.onXMinChange(Number(l.target.value)||0)}Wt(t,{children:(l,y)=>{var b=$c(),S=Y(b);$e(S,{text:"Cases",children:(R,$)=>{Bt(R,{class:"w-[160px]",get options(){return e(i)},get selected(){return r.selectedCases},get onchange(){return r.onCasesChange},placeholder:"Select cases..."})}});var w=A(S,2);{var T=R=>{var $=Ke(),h=Y($);ct(h,()=>r.middleSlot),f(R,$)};G(w,R=>{r.middleSlot&&R(T)})}var N=A(w,2);$e(N,{get text(){return a()},children:(R,$)=>{Bt(R,{class:"w-[160px]",get options(){return e(o)},get selected(){return r.selectedColumns},get onchange(){return r.onColumnsChange},placeholder:"Select..."})}});var x=A(N,2);$e(x,{text:"Start from",children:(R,$)=>{st(R,{class:"w-[140px]",get options(){return u},get value(){return r.startFrom},onchange:h=>r.onStartFromChange(h)})}});var c=A(x,2);{var m=R=>{$e(R,{get text(){return n()},children:($,h)=>{var E=Ec();qe(E),Q(()=>gn(E,r.xMin)),ie("input",E,v),f($,E)}})};G(c,R=>{r.startFrom==="custom"&&R(m)})}var P=A(c,2);{var _=R=>{var $=Ke(),h=Y($);ct(h,()=>r.extraSlot),f(R,$)};G(P,R=>{r.extraSlot&&R(_)})}f(l,b)}}),Ne()}Be(["input"]);var Mc=M('
      '),Nc=M('
      ');function Gt(t,r){let a=fe(r,"emptyMessage",3,"");var n=Nc(),i=k(n);{var o=v=>{var l=Ke(),y=Y(l);Tr(y,()=>r.svgHtml),f(v,l)},u=v=>{var l=Mc(),y=k(l),b=k(y,!0);p(y),p(l),Q(()=>re(b,a()||"No data to display.")),f(v,l)};G(i,v=>{r.svgHtml?v(o):v(u,-1)})}p(n),Q(()=>ye(n,"id",r.id)),f(t,n)}async function En(t,r="plot.png"){const a=document.getElementById(t),n=a==null?void 0:a.querySelector("svg");if(!n)return;const{width:i,height:o}=Ac(n),u=window.devicePixelRatio||1,v=document.createElement("canvas");v.width=i*u,v.height=o*u;const l=v.getContext("2d");if(!l)return;l.scale(u,u);const y=new XMLSerializer().serializeToString(n),b=new Blob([y],{type:"image/svg+xml;charset=utf-8"}),S=URL.createObjectURL(b),w=new Image;w.width=i,w.height=o,await new Promise((N,x)=>{w.onload=()=>{l.drawImage(w,0,0,i,o),URL.revokeObjectURL(S),N()},w.onerror=x,w.src=S});const T=await new Promise(N=>v.toBlob(N,"image/png"));T&&await Ia(T,Tc(r))}function _r(t,r,a="png"){const n=r.length<=3?r.join("_"):`${r.length}_cases`,i=new Date().toISOString().replace(/[:.]/g,"-").slice(0,19);return Lc(`${t}_${n}_${i}.${a}`)}async function za(t,r){const a=new Blob([t],{type:"text/csv;charset=utf-8"});await Ia(a,r)}function Ac(t){const r=t.getAttribute("viewBox");if(r){const a=r.split(/[\s,]+/).map(Number);if(a.length===4)return{width:a[2],height:a[3]}}return{width:t.width.baseVal.value||900,height:t.height.baseVal.value||500}}async function Ia(t,r){if("showSaveFilePicker"in window)try{const o=await(await window.showSaveFilePicker({suggestedName:r,types:[{description:t.type.startsWith("image/")?"Image":"File",accept:{[t.type]:[`.${r.split(".").pop()}`]}}]})).createWritable();await o.write(t),await o.close();return}catch(i){if((i==null?void 0:i.name)==="AbortError")return}const a=URL.createObjectURL(t),n=document.createElement("a");n.href=a,n.download=r,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(a)}function Tc(t){return t.endsWith(".png")?t:`${t}.png`}function Lc(t){return t.replace(/[^a-zA-Z0-9._-]/g,"_")}let Oa=F(se([])),Ha=F(se([])),ja=F(se([])),Ua=F("zero"),Va=F(0),Ba=F(""),Dc=F(!0);function Fc(){return{selectedCases:e(Oa),columns:e(Ha),selectedColumns:e(ja),startFrom:e(Ua),iterMin:e(Va),svgHtml:e(Ba),autoRefresh:e(Dc)}}function ta(t){d(Oa,t,!0)}function zc(t){d(Ha,t,!0)}function Gr(t){d(ja,t,!0)}function Ic(t){d(Ua,t,!0)}function Oc(t){d(Va,t,!0)}function kr(t){d(Ba,t,!0)}var Hc=M(" Refresh",1),jc=M(" ",1),Uc=M(" Download as PNG",1),Vc=M('
      '),Bc=M('
      ',1),qc=M('
      '),Kc=M('

      '),Wc=M(" ",1);function Gc(t,r){Me(r,!0);let a=z(Fc),n=F("");async function i(){var T;if(e(a).selectedCases.length)try{const N=(await Ls(e(a).selectedCases)).filter(x=>x!=="iteration");if(d(n,""),zc(N),N.length===0){Gr([]),kr("");return}if(e(a).selectedColumns.length===0){const x=(((T=Pt())==null?void 0:T.default_residual_columns)??[]).filter(c=>N.includes(c));Gr(x.length>0?x:N.slice(0,2))}}catch(N){d(n,"Failed to load residual columns"),console.error("Failed to load residual columns:",N)}}async function o(){if(e(a).selectedCases.length&&!((e(a).columns.length===0||e(a).selectedColumns.length===0)&&(await i(),!e(a).selectedColumns.length)))try{let T=0,N=!0;if(e(a).startFrom==="restart"){const c=await Ra(e(a).selectedCases),m=Object.values(c.origins).map(P=>P.iteration).filter(P=>P!==void 0&&Number.isFinite(P));T=m.length>0?Math.min(...m):0}else e(a).startFrom==="custom"&&(T=e(a).iterMin);const x=await Ds(e(a).selectedCases,e(a).selectedColumns,{xMin:T,includeHistory:N});d(n,""),x&&kr(x)}catch(T){d(n,"Failed to load residual plot"),console.error("Failed to load residual plot:",T)}}function u(T){if(ta(T),T.length===0){kr("");return}i(),o()}function v(T){if(Gr(T),T.length===0){kr("");return}o()}function l(T){Ic(T),o()}function y(T){Oc(T),o()}let b=F(se(Qt("plot")));Ee(()=>{Jt("plot",e(b))}),Ee(()=>{e(b)?hr("residualPlot",o,Kn()):ft("residualPlot")});let S=!1;Ee(()=>{r.allCases.length>0&&!S&&(S=!0,ta([...r.allCases]),i().then(()=>o()))});const w=pr(()=>{S&&i().then(()=>o())});dr(()=>{ft("residualPlot"),w()}),Ht(t,{eyebrow:"Diagnostics",title:"Residuals Plot",wide:!0,actions:N=>{var x=jc(),c=Y(x);{let _=z(Kn);Zt(c,{name:"residualPlot",get intervalMs(){return e(_)},onRefresh:o,get checked(){return e(b)},set checked(R){d(b,R,!0)}})}var m=A(c,2);{var P=_=>{Re(_,{variant:"primary",onclick:o,children:(R,$)=>{var h=Hc(),E=Y(h);Le(E,{get icon(){return Yt}}),Ce(),f(R,h)},$$slots:{default:!0}})};G(m,_=>{e(b)||_(P)})}f(N,x)},children:(N,x)=>{var c=Wc(),m=Y(c);{var P=h=>{const E=V=>{var O=Vc(),B=k(O);{let U=z(()=>!e(a).svgHtml);Re(B,{variant:"secondary",size:"sm",onclick:()=>En("plot-holder",_r("residuals",e(a).selectedCases)),get disabled(){return e(U)},children:(q,g)=>{var s=Uc(),C=Y(s);Le(C,{get icon(){return gr}}),Ce(),f(q,s)},$$slots:{default:!0}})}p(O),f(V,O)};var D=Bc(),L=Y(D);Rn(L,{prefix:"plot",get allCases(){return r.allCases},get selectedCases(){return e(a).selectedCases},onCasesChange:u,columnLabel:"Variables",get columns(){return e(a).columns},get selectedColumns(){return e(a).selectedColumns},onColumnsChange:v,get startFrom(){return e(a).startFrom},onStartFromChange:l,xMinLabel:"Iter min",get xMin(){return e(a).iterMin},onXMinChange:y,get extraSlot(){return E}});var j=A(L,2),I=k(j);{let V=z(()=>e(a).selectedCases.length===0?"Please select at least one case.":e(a).columns.length===0?"No residuals for the selected cases yet.":e(a).selectedColumns.length===0?"Please select at least one variable.":"No data to display.");Gt(I,{id:"plot-holder",get svgHtml(){return e(a).svgHtml},get emptyMessage(){return e(V)}})}p(j),f(h,D)},_=h=>{var E=qc(),D=k(E);Gt(D,{id:"plot-holder",svgHtml:"",emptyMessage:"No data available. Please run a simulation first."}),p(E),f(h,E)};G(m,h=>{r.allCases.length>0?h(P):h(_,-1)})}var R=A(m,2);{var $=h=>{var E=Kc(),D=k(E,!0);p(E),Q(()=>re(D,e(n))),f(h,E)};G(R,h=>{e(n)&&h($)})}f(N,c)},$$slots:{actions:!0,default:!0}}),Ne()}function qa(t){const{scope:r,getState:a,setState:n,getAxis:i}=t;async function o(){const S=a();if(S.selectedCases.length)try{let w=await Jr(S.selectedCases,r);if(t.filterFiles&&(w=t.filterFiles(w)),n({files:w}),w.length===0){n({file:"",columns:[],selectedColumns:[],positionText:"",svgHtml:""});return}(!S.file||!w.includes(S.file))&&(n({file:w[0]}),await u())}catch(w){console.error(`Failed to load ${r} files:`,w)}}async function u(){const S=a();if(!(!S.selectedCases.length||!S.file))try{const w=await Os(S.selectedCases,[S.file]),T=t.filterColumns?t.filterColumns(w):w;t.onColumnsLoaded?t.onColumnsLoaded(w):n({columns:T});const N=a();if(N.columns.length===0){n({selectedColumns:[],positionText:"",svgHtml:""});return}N.selectedColumns.length===0&&n({selectedColumns:[N.columns[0]]})}catch(w){console.error(`Failed to load ${r} columns:`,w)}}async function v(){let S=a();if(S.selectedCases.length){if(await o(),S=a(),!S.file){n({positionText:"",svgHtml:""});return}if((S.columns.length===0||S.selectedColumns.length===0)&&(await u(),S=a(),!S.selectedColumns.length)){n({positionText:"",svgHtml:""});return}try{let w;const T=a();if(T.startFrom==="restart"){const x=await Ra(T.selectedCases),c=i().toLowerCase(),m=c==="t"||c==="time"?"time":"iteration",P=Object.values(x.origins).map(_=>_[m]).filter(_=>_!==void 0&&Number.isFinite(_));w=P.length>0?Math.min(...P):void 0}else T.startFrom==="custom"&&(w=T.xMin);const N=await js({cases:T.selectedCases,probes:[T.file],columns:T.selectedColumns,axis:i(),timeMin:typeof w=="number"?w:void 0,xMin:w,includeHistory:!0});N&&n({svgHtml:N})}catch(w){console.error(`Failed to load ${r} plot:`,w)}}}async function l(S){if(n({selectedCases:S}),S.length===0){n({svgHtml:""});return}await o(),r==="probes"&&await v()}function y(S){if(n({selectedColumns:S}),S.length===0){n({svgHtml:""});return}v()}async function b(S){n({file:S}),await u(),await v()}return{loadFiles:o,loadColumns:u,loadPlot:v,handleCasesChange:l,handleColumnsChange:y,handleFileChange:b}}function Ka(){return{selectedCases:[],file:"",files:[],columns:[],selectedColumns:[],axis:"time",startFrom:"zero",xMin:0,positionText:"",svgHtml:"",autoRefresh:!0}}let Ut=F("time"),ln=F(se(Ka())),cn=F(se({...Ka(),axis:""})),Xc=F(!0),Mr=F(!1),Nr=F(!1);function Yc(){return e(Ut)}function Qc(t){d(Ut,t,!0)}function Jc(){return e(Mr)}function ra(t){d(Mr,t,!0)}function Zc(){return e(Nr)}function na(t){d(Nr,t,!0)}function eu(){e(Ut)==="time"&&!e(Mr)&&e(Nr)?d(Ut,"profile"):e(Ut)==="profile"&&!e(Nr)&&e(Mr)&&d(Ut,"time")}function aa(){return e(ln)}function Xr(){return e(cn)}function yt(t){d(ln,{...e(ln),...t},!0)}function Tt(t){d(cn,{...e(cn),...t},!0)}function tu(){return e(Xc)}var ru=M(" Download as PNG",1),nu=M('
      '),au=M("
      "),ou=M('
      '),su=M('
      ',1),iu=M('
      ');function lu(t,r){Me(r,!0);let a=fe(r,"onRefresh",15),n=z(aa);const i=new Set(["time","t","iteration","iter"]),o=qa({scope:"probes",getState:aa,setState:yt,filterFiles:x=>x.filter(c=>{const m=c.replace(/\.csv$/i,"").toLowerCase();return m!=="coords"&&!m.includes("coordinates")}),filterColumns:x=>x.filter(c=>!i.has(c.toLowerCase())),onColumnsLoaded:x=>{const c=x.filter(m=>!i.has(m.toLowerCase()));yt({columns:c}),u()},getAxis:()=>"time"});async function u(){if(!(!e(n).selectedCases.length||!e(n).file||!e(n).selectedColumns.length))try{const x=[];for(const $ of e(n).selectedColumns){const h=await Hs(e(n).selectedCases[0],e(n).file,[$]);h.found&&x.push({col:$,x:h.x,y:h.y,z:h.z})}if(!x.length){yt({positionText:""});return}const c=$=>{let h=0;for(const E of $){if(E===void 0)continue;const D=String(E),L=D.indexOf(".");L>=0&&(h=Math.max(h,D.length-L-1))}return h},m=c(x.map($=>$.x)),P=c(x.map($=>$.y)),_=c(x.map($=>$.z)),R=x.map($=>{const h=[];return $.x!==void 0&&h.push(`x=${$.x.toFixed(m)}`),$.y!==void 0&&h.push(`y=${$.y.toFixed(P)}`),$.z!==void 0&&h.push(`z=${$.z.toFixed(_)}`),`Probe position [${$.col}] : ${h.join(" · ")}`});yt({positionText:R.join(` +`)})}catch{yt({positionText:""})}}function v(x){o.handleColumnsChange(x),x.length>0&&u()}let l=z(()=>e(n).files.map(x=>({value:x,label:x.replace(/\.csv$/i,"").replace(/^probes?_/i,"")}))),y=!1;Ee(()=>{r.allCases.length>0&&!y&&(y=!0,yt({selectedCases:[...r.allCases]}),o.loadFiles().then(()=>o.loadPlot()))}),Ee(()=>{a(o.loadPlot)});let b=z(()=>e(n).selectedCases.length===0?"Please select at least one case.":e(n).files.length===0?"No probe data for the selected cases yet.":e(n).selectedColumns.length===0?"Please select at least one probe.":"No data to display.");var S=Ke(),w=Y(S);{var T=x=>{const c=D=>{$e(D,{text:"Quantity",children:(L,j)=>{st(L,{class:"w-[160px]",get options(){return e(l)},get value(){return e(n).file},get onchange(){return o.handleFileChange}})}})},m=D=>{var L=nu(),j=k(L);{let I=z(()=>!e(n).svgHtml);Re(j,{variant:"secondary",size:"sm",onclick:()=>En("probe-plot-holder",_r("probe",e(n).selectedCases)),get disabled(){return e(I)},children:(V,O)=>{var B=ru(),U=Y(B);Le(U,{get icon(){return gr}}),Ce(),f(V,B)},$$slots:{default:!0}})}p(L),f(D,L)};var P=su(),_=Y(P);Rn(_,{prefix:"probe",get allCases(){return r.allCases},get selectedCases(){return e(n).selectedCases},get onCasesChange(){return o.handleCasesChange},get middleSlot(){return c},columnLabel:"Probes",get columns(){return e(n).columns},get selectedColumns(){return e(n).selectedColumns},onColumnsChange:v,get startFrom(){return e(n).startFrom},onStartFromChange:D=>{yt({startFrom:D}),o.loadPlot()},xMinLabel:"Time min",get xMin(){return e(n).xMin},onXMinChange:D=>{yt({xMin:D}),o.loadPlot()},get extraSlot(){return m}});var R=A(_,2);{var $=D=>{var L=ou();je(L,21,()=>e(n).positionText.split(` +`).sort(),vn,(j,I)=>{var V=au(),O=k(V,!0);p(V),Q(()=>re(O,e(I))),f(j,V)}),p(L),f(D,L)};G(R,D=>{e(n).positionText&&D($)})}var h=A(R,2),E=k(h);Gt(E,{id:"probe-plot-holder",get svgHtml(){return e(n).svgHtml},get emptyMessage(){return e(b)}}),p(h),f(x,P)},N=x=>{var c=iu(),m=k(c);Gt(m,{id:"probe-plot-holder",svgHtml:"",emptyMessage:"No probe data available. Please run a simulation first."}),p(c),f(x,c)};G(w,x=>{r.allCases.length>0?x(T):x(N,-1)})}f(t,S),Ne()}var cu=M(" ",1),uu=M(" Download as PNG",1),du=M('
      '),fu=M('
      ',1),vu=M('
      ');function gu(t,r){Me(r,!0);let a=fe(r,"onRefresh",15),n=z(Xr);const i=["s","x","abscissa","distance","arclength","arc_length","curvilinear","length","r","y","z","coord","position"];function o(c){const m=c.map(P=>P.toLowerCase());for(const P of i){const _=m.indexOf(P);if(_>=0)return c[_]}return c[0]??""}const u=qa({scope:"profiles",getState:Xr,setState:Tt,onColumnsLoaded:c=>{const m=e(n).axis||o(c),P=c.filter(_=>_!==m).toSorted((_,R)=>_.localeCompare(R));Tt({columns:P,axis:m,selectedColumns:e(n).selectedColumns.length?e(n).selectedColumns:P.length?[P[0]]:[]})},getAxis:()=>Xr().axis});function v(c){const m=e(n).axis;Tt({axis:c});const _=[...e(n).columns,...m?[m]:[]].filter(R=>R!==c).toSorted((R,$)=>R.localeCompare($));Tt({columns:_}),u.loadPlot()}let l=z(()=>e(n).files.map(c=>({value:c,label:c.replace(/^.*\//,"").replace(/\.csv$/i,"").replace(/_/g," ")}))),y=z(()=>[...e(n).axis?[e(n).axis]:[],...e(n).columns].toSorted((c,m)=>c.localeCompare(m)).map(c=>({value:c,label:c}))),b=!1;Ee(()=>{r.allCases.length>0&&!b&&(b=!0,Tt({selectedCases:[...r.allCases]}),u.loadFiles().then(()=>u.loadPlot()))}),Ee(()=>{a(u.loadPlot)});let S=z(()=>e(n).selectedCases.length===0?"Please select at least one case.":e(n).files.length===0?"No profile data for the selected cases yet.":e(n).selectedColumns.length===0?"Please select at least one value.":"No data to display.");var w=Ke(),T=Y(w);{var N=c=>{const m=E=>{var D=cu(),L=Y(D);$e(L,{text:"Profile",children:(I,V)=>{st(I,{class:"w-[160px]",get options(){return e(l)},get value(){return e(n).file},get onchange(){return u.handleFileChange}})}});var j=A(L,2);$e(j,{text:"X axis",children:(I,V)=>{st(I,{class:"w-[140px]",get options(){return e(y)},get value(){return e(n).axis},onchange:v})}}),f(E,D)},P=E=>{var D=du(),L=k(D);{let j=z(()=>!e(n).svgHtml);Re(L,{variant:"secondary",size:"sm",onclick:()=>En("profile-plot-holder",_r("profile",e(n).selectedCases)),get disabled(){return e(j)},children:(I,V)=>{var O=uu(),B=Y(O);Le(B,{get icon(){return gr}}),Ce(),f(I,O)},$$slots:{default:!0}})}p(D),f(E,D)};var _=fu(),R=Y(_);Rn(R,{prefix:"profile",get allCases(){return r.allCases},get selectedCases(){return e(n).selectedCases},get onCasesChange(){return u.handleCasesChange},get middleSlot(){return m},columnLabel:"Values",get columns(){return e(n).columns},get selectedColumns(){return e(n).selectedColumns},get onColumnsChange(){return u.handleColumnsChange},get startFrom(){return e(n).startFrom},onStartFromChange:E=>{Tt({startFrom:E}),u.loadPlot()},xMinLabel:"X min",get xMin(){return e(n).xMin},onXMinChange:E=>{Tt({xMin:E}),u.loadPlot()},get extraSlot(){return P}});var $=A(R,2),h=k($);Gt(h,{id:"profile-plot-holder",get svgHtml(){return e(n).svgHtml},get emptyMessage(){return e(S)}}),p($),f(c,_)},x=c=>{var m=vu(),P=k(m);Gt(P,{id:"profile-plot-holder",svgHtml:"",emptyMessage:"No profile data available. Please run a simulation first."}),p(m),f(c,m)};G(T,c=>{r.allCases.length>0?c(N):c(x,-1)})}f(t,w),Ne()}var hu=M('
      Diagnostics
      '),pu=M(" Refresh",1),mu=M(" ",1);function _u(t,r){Me(r,!0);let a=z(Yc),n=z(tu),i=z(Jc),o=z(Zc),u=F(se(Qt("probe")));Ee(()=>{Jt("probe",e(u))});let v=F(void 0);function l(){var N;(N=e(v))==null||N()}async function y(){if(r.allCases.length){try{const x=(await Jr(r.allCases,"probes")).filter(c=>{const m=c.replace(/\.csv$/i,"").toLowerCase();return m!=="coords"&&!m.includes("coordinates")});ra(x.length>0)}catch{ra(!1)}try{const N=await Jr(r.allCases,"profiles");na(N.length>0)}catch{na(!1)}}}Ee(()=>{r.allCases.length>0&&y()}),pr(()=>{y(),l()}),Ee(()=>{e(i),e(o),eu()});function b(N){N==="time"&&!e(i)||N==="profile"&&!e(o)||Qc(N)}var S=Ke(),w=Y(S);{var T=N=>{const x=c=>{var m=hu(),P=A(k(m),2),_=k(P),R=A(_,2);p(P),p(m),Q(()=>{ke(_,1,`bg-transparent border-none p-0 m-0 text-lg font-bold tracking-tight transition-colors duration-150 + ${e(i)?e(a)==="time"?"text-edf-bleu-fonce cursor-pointer":"text-edf-gris-moyen cursor-pointer hover:text-muted":"text-edf-gris-moyen cursor-not-allowed opacity-50"}`),_.disabled=!e(i),ye(_,"title",e(i)?"":"No probe data available. Run a simulation first."),ke(R,1,`bg-transparent border-none p-0 m-0 text-lg font-bold tracking-tight transition-colors duration-150 + ${e(o)?e(a)==="profile"?"text-edf-bleu-fonce cursor-pointer":"text-edf-gris-moyen cursor-pointer hover:text-muted":"text-edf-gris-moyen cursor-not-allowed opacity-50"}`),R.disabled=!e(o),ye(R,"title",e(o)?"":"No profile data available. Run a simulation first.")}),ie("click",_,()=>b("time")),ie("click",R,()=>b("profile")),f(c,m)};Ht(N,{wide:!0,get titleSlot(){return x},actions:m=>{var P=mu(),_=Y(P);{let h=z(()=>e(a)==="time"?"probePlot":"profilePlot"),E=z(ol);Zt(_,{get name(){return e(h)},get intervalMs(){return e(E)},onRefresh:l,get checked(){return e(u)},set checked(D){d(u,D,!0)}})}var R=A(_,2);{var $=h=>{Re(h,{variant:"primary",onclick:l,children:(E,D)=>{var L=pu(),j=Y(L);Le(j,{get icon(){return Yt}}),Ce(),f(E,L)},$$slots:{default:!0}})};G(R,h=>{e(u)||h($)})}f(m,P)},children:(m,P)=>{var _=Ke(),R=Y(_);{var $=E=>{lu(E,{get allCases(){return r.allCases},get onRefresh(){return e(v)},set onRefresh(D){d(v,D,!0)}})},h=E=>{gu(E,{get allCases(){return r.allCases},get onRefresh(){return e(v)},set onRefresh(D){d(v,D,!0)}})};G(R,E=>{e(a)==="time"?E($):E(h,-1)})}f(m,_)},$$slots:{actions:!0,default:!0}})};G(w,N=>{e(n)&&N(T)})}f(t,S),Ne()}Be(["click"]);var bu=M(" Refresh",1),xu=M(" ",1),yu=M(" Download as CSV",1),wu=M('
      ',1),ku=M(" "),Cu=M(' '),Su=M(' '),Pu=M('
      Case
      '),Ru=M('

      '),Eu=M(" ",1);function $u(t,r){Me(r,!0);let a=F(se([])),n=F(se([])),i=F(!1),o=F(!1),u=F(se(Qt("perf")));Ee(()=>{Jt("perf",e(u))});const v=5e3;let l=F(se([])),y=z(()=>r.allCases.map(m=>({value:m,label:m}))),b=z(()=>e(l).length>0&&e(n).length>0);async function S(){if(e(a).length){d(o,!0);try{const m=await jn(e(a));d(l,m.columns,!0),d(n,m.records,!0),e(b)&&d(i,!0)}catch(m){console.error("Failed to load perf:",m)}d(o,!1)}}function w(m){if(d(a,m,!0),m.length===0){d(n,[],!0);return}S()}function T(m,P){if(P==null||P==="")return"-";if(m==="text")return String(P);const _=Number(P);return Number.isFinite(_)?m==="int"?String(Math.round(_)):_.toFixed(3):String(P)}async function N(){if(!e(n).length)return;const m=["case_id",...e(l).map($=>$.key)],P=e(n).map($=>[$.case_id,...e(l).map(h=>$[h.key]??"")].join(",")),_=[m.join(","),...P].join(` +`),R=_r("timing_snapshot",e(a),"csv");await za(_,R)}let x=!1;Ee(()=>{r.allCases.length>0&&!x&&(x=!0,d(a,[...r.allCases],!0),jn(r.allCases).then(m=>{d(l,m.columns,!0),d(n,m.records,!0),d(i,e(b),!0)}).catch(()=>{}))}),Ee(()=>{e(u)?hr("perf",S,v):ft("perf")});const c=pr(()=>{x&&S()});dr(()=>{ft("perf"),c()}),Ht(t,{eyebrow:"Performance",title:"Timing Snapshot",wide:!0,actions:P=>{var _=xu(),R=Y(_);Zt(R,{name:"perf",intervalMs:v,onRefresh:S,get checked(){return e(u)},set checked(E){d(u,E,!0)}});var $=A(R,2);{var h=E=>{Re(E,{variant:"primary",onclick:S,children:(D,L)=>{var j=bu(),I=Y(j);Le(I,{get icon(){return Yt}}),Ce(),f(D,j)},$$slots:{default:!0}})};G($,E=>{e(u)||E(h)})}f(P,_)},children:(P,_)=>{var R=Eu(),$=Y(R);{var h=j=>{Wt(j,{children:(I,V)=>{var O=wu(),B=Y(O);$e(B,{text:"Cases",children:(g,s)=>{Bt(g,{class:"w-[160px]",get options(){return e(y)},get selected(){return e(a)},onchange:w,placeholder:"Select cases..."})}});var U=A(B,2),q=k(U);{let g=z(()=>!e(b));Re(q,{variant:"secondary",size:"sm",onclick:N,get disabled(){return e(g)},children:(s,C)=>{var H=yu(),W=Y(H);Le(W,{get icon(){return gr}}),Ce(),f(s,H)},$$slots:{default:!0}})}p(U),f(I,O)}})};G($,j=>{e(i)&&j(h)})}var E=A($,2);{var D=j=>{var I=Pu(),V=k(I),O=k(V),B=k(O),U=A(k(B));je(U,17,()=>e(l),g=>g.key,(g,s)=>{var C=ku(),H=k(C,!0);p(C),Q(()=>re(H,e(s).label)),f(g,C)}),p(B),p(O);var q=A(O);je(q,21,()=>e(n),g=>g.case_id,(g,s)=>{var C=Su(),H=k(C),W=k(H,!0);p(H);var ee=A(H);je(ee,17,()=>e(l),ne=>ne.key,(ne,me)=>{var K=Cu(),X=k(K,!0);p(K),Q(ce=>re(X,ce),[()=>T(e(me).kind,e(s)[e(me).key])]),f(ne,K)}),p(C),Q(()=>re(W,e(s).case_id)),f(g,C)}),p(q),p(V),p(I),f(j,I)},L=j=>{var I=Ru(),V=k(I);{var O=U=>{var q=Ie("Select cases above to view timing information.");f(U,q)},B=U=>{var q=Ie("No performance data available. Please run a simulation first.");f(U,q)};G(V,U=>{e(i)?U(O):U(B,-1)})}p(I),f(j,I)};G(E,j=>{e(b)?j(D):j(L,-1)})}f(P,R)},$$slots:{actions:!0,default:!0}}),Ne()}const Mu={"&":"&","<":"<",">":">",'"':""","'":"'"};function Wa(t){return t.replace(/[&<>"']/g,r=>Mu[r]??r)}var Nu=M("
      "),Au=M('
      ');function Tu(t,r){Me(r,!0);let a=F(void 0);function n(y){const b=Wa(y);if(!r.searchQuery.trim())return b;try{const S=new RegExp(`(${r.searchQuery})`,"gi");return b.replace(S,'$1')}catch{return b}}function i(y){return y==="error"?"tail-sev-error":y==="warn"?"tail-sev-warn":""}function o(){return e(a)?e(a).scrollHeight-e(a).scrollTop-e(a).clientHeight<=24:!0}let u=F(!0);function v(){d(u,o(),!0)}Ee(()=>{r.autoScroll&&r.lines.length>0&&e(a)&&e(u)&&St().then(()=>{e(a)&&(e(a).scrollTop=e(a).scrollHeight)})});var l=Au();je(l,21,()=>r.lines,y=>y.index,(y,b)=>{var S=Nu();Tr(S,()=>n(e(b).text),!0),p(S),Q(w=>ke(S,1,`tail-line ${w??""} ${e(b).isNew?"tail-new":""}`),[()=>i(e(b).severity)]),f(y,S)}),p(l),tt(l,y=>d(a,y),()=>e(a)),Ze("scroll",l,v),f(t,l),Ne()}var Lu=M(" Refresh",1),Du=M(" ",1),Fu=M(''),zu=M(''),Iu=M("   ",1),Ou=M(" ",1),Hu=Ot(''),ju=Ot(''),Uu=Ot(''),Vu=M(" Resume",1),Bu=M(" Pause",1),qu=M('Case not running'),Ku=M(' 
      ',1),Wu=M('

      '),Gu=M('

      '),Xu=M(" ",1);function Yu(t,r){Me(r,!0);const a=80;let n=F(""),i=F(""),o=F(!1),u=F(se([])),v=F(a),l=F(se([])),y=F(se(new Set)),b=F(!1),S=F(se(Qt("tail")));Ee(()=>{Jt("tail",e(S))});let w=F(!0),T=F("all"),N=F(""),x=F(""),c=F(void 0),m=z(()=>xn(r.allCases)),P=z(()=>e(u).map(U=>{const q=U.split("/").pop()??U;return{value:U,label:q}})),_=[{value:"all",label:"All"},{value:"error",label:"Error"},{value:"warn",label:"Warn"}],R=z(()=>e(u).length>0),$=z(()=>Fr().some(U=>{var q;return U.case_id===e(n)&&((q=U.status)==null?void 0:q.toUpperCase())==="RUNNING"}));async function h(){if(e(n)){try{d(u,await zs(e(n)),!0)}catch(U){console.error("Failed to load tail files:",U),d(u,[],!0);return}(!e(o)||!e(u).includes(e(i)))&&(d(i,e(u)[0]??"",!0),d(o,!1)),e(i)||d(l,[],!0)}}async function E(U=!1){if(e(n)&&!(e(b)&&!U)&&(await h(),!!e(i)))try{const q=(await Is(e(n),e(i),e(v))).lines;if(d(x,""),e(l).length>0){const g=D(e(l),q),s=new Set;for(let C=g;C{let U=e(l).map((q,g)=>({...q,index:g,isNew:e(y).has(g)}));if(e(T)!=="all"&&(U=U.filter(q=>q.severity===e(T))),e(N).trim())try{const q=new RegExp(e(N),"i");U=U.filter(g=>q.test(g.text))}catch{}return U});function j(U){d(n,U,!0),d(i,""),d(o,!1),d(l,[],!0),h().then(()=>E(!0))}function I(U){d(i,U,!0),d(o,!0),d(l,[],!0),E(!0)}function V(){d(b,!e(b)),e(b)||E(!0)}function O(U){(U.ctrlKey||U.metaKey)&&U.key==="f"&&e(c)&&(U.preventDefault(),e(c).focus())}Ee(()=>{r.allCases.length>0&&!e(n)&&(d(n,r.allCases[0],!0),h().then(()=>E(!0)))}),Ee(()=>{e(S)&&!e(b)&&e(n)?hr("tail",()=>E(!1),qn()):ft("tail")});const B=pr(()=>{e(n)&&h().then(()=>E(!0))});dr(()=>{ft("tail"),B()}),Ze("keydown",ot,O),Ht(t,{eyebrow:"Live",title:"Log Tail",wide:!0,actions:q=>{var g=Du(),s=Y(g);{let W=z(qn);Zt(s,{name:"tail",get intervalMs(){return e(W)},onRefresh:()=>E(!1),get checked(){return e(S)},set checked(ee){d(S,ee,!0)}})}var C=A(s,2);{var H=W=>{Re(W,{variant:"primary",onclick:()=>E(!0),children:(ee,ne)=>{var me=Lu(),K=Y(me);Le(K,{get icon(){return Yt}}),Ce(),f(ee,me)},$$slots:{default:!0}})};G(C,W=>{e(S)||W(H)})}f(q,g)},children:(q,g)=>{var s=Xu(),C=Y(s);Wt(C,{children:(K,X)=>{var ce=Ou(),le=Y(ce);$e(le,{text:"Case",children:(Z,_e)=>{st(Z,{class:"w-[130px]",get options(){return e(m)},get value(){return e(n)},onchange:j})}});var we=A(le,2);{var he=Z=>{var _e=Iu(),Se=Y(_e);$e(Se,{text:"File",children:(be,Oe)=>{st(be,{class:"w-[150px]",get options(){return e(P)},get value(){return e(i)},onchange:I})}});var Pe=A(Se,2);$e(Pe,{text:"Lines",children:(be,Oe)=>{var Fe=Fu();qe(Fe),ie("change",Fe,()=>E(!0)),Je(Fe,()=>e(v),oe=>d(v,oe)),f(be,Fe)}});var ge=A(Pe,2);$e(ge,{text:"Filter",children:(be,Oe)=>{var Fe=zu();qe(Fe),tt(Fe,oe=>d(c,oe),()=>e(c)),Je(Fe,()=>e(N),oe=>d(N,oe)),f(be,Fe)}});var De=A(ge,2);$e(De,{text:"Severity",children:(be,Oe)=>{st(be,{class:"w-[90px]",get options(){return _},get value(){return e(T)},onchange:Fe=>d(T,Fe,!0)})}}),f(Z,_e)};G(we,Z=>{e(R)&&Z(he)})}f(K,ce)}});var H=A(C,2);{var W=K=>{var X=Ku(),ce=Y(X);Tu(ce,{get lines(){return e(L)},get searchQuery(){return e(N)},get autoScroll(){return e(w)}});var le=A(ce,2),we=k(le),he=k(we);{var Z=oe=>{var ue=Hu();f(oe,ue)},_e=oe=>{var ue=ju();f(oe,ue)},Se=oe=>{var ue=Uu();f(oe,ue)};G(he,oe=>{e(b)?oe(Z):e($)?oe(_e,1):oe(Se,-1)})}var Pe=A(he);p(we);var ge=A(we,2);{var De=oe=>{Re(oe,{variant:"secondary",size:"sm",onclick:V,children:(ue,He)=>{var te=Ke(),J=Y(te);{var ae=xe=>{var Ae=Vu(),pe=Y(Ae);Le(pe,{get icon(){return Na}}),Ce(),f(xe,Ae)},de=xe=>{var Ae=Bu(),pe=Y(Ae);Le(pe,{get icon(){return wi}}),Ce(),f(xe,Ae)};G(J,xe=>{e(b)?xe(ae):xe(de,-1)})}f(ue,te)},$$slots:{default:!0}})},be=oe=>{var ue=qu();f(oe,ue)};G(ge,oe=>{e($)?oe(De):oe(be,-1)})}var Oe=A(ge,2),Fe=k(Oe);It(Fe,{get checked(){return e(w)},onchange:oe=>d(w,oe,!0),size:14,label:"Auto-scroll",labelFirst:!0}),p(Oe),p(le),Q(()=>re(Pe,` ${e(L).length??""} / ${e(l).length??""}`)),f(K,X)},ee=K=>{var X=Wu(),ce=k(X);{var le=he=>{var Z=Ie("No log file yet for this case.");f(he,Z)},we=he=>{var Z=Ie("No log data available. Please run a simulation first.");f(he,Z)};G(ce,he=>{e(n)&&!e(R)?he(le):he(we,-1)})}p(X),f(K,X)};G(H,K=>{e(l).length>0?K(W):K(ee,-1)})}var ne=A(H,2);{var me=K=>{var X=Gu(),ce=k(X,!0);p(X),Q(()=>re(ce,e(x))),f(K,X)};G(ne,K=>{e(x)&&K(me)})}f(q,s)},$$slots:{actions:!0,default:!0}}),Ne()}Be(["change"]);function Qu(t,r=3){var T,N;const a=t.split(` +`),n=[];let i=0,o=0,u=!1;for(const x of a){if(x.startsWith("===")||x.startsWith("---")||x.startsWith("+++"))continue;const c=x.match(/^@@ -(\d+),?\d* \+(\d+),?\d* @@/);if(c){i=parseInt(c[1],10)-1,o=parseInt(c[2],10)-1,u=!0;continue}if(u)if(x.startsWith("-"))i++,n.push({type:"del",leftNum:i,rightNum:null,leftContent:x.slice(1),rightContent:""});else if(x.startsWith("+"))o++,n.push({type:"add",leftNum:null,rightNum:o,leftContent:"",rightContent:x.slice(1)});else{i++,o++;const m=x.startsWith(" ")?x.slice(1):x;n.push({type:"equal",leftNum:i,rightNum:o,leftContent:m,rightContent:m})}}const v=[];let l=0;for(;l=0&&x-S>1&&b.push({kind:"separator",skipped:x-S-1});const c=v[x].type!=="equal",m=c&&!w;b.push({kind:"line",row:v[x],hunkStart:m}),w=c,S=x}if(S=0&&b.push({kind:"separator",skipped:v.length-1-S}),b.length>0&&b[0].kind==="line"){const x=((T=b[0].row)==null?void 0:T.leftNum)??0,c=((N=b[0].row)==null?void 0:N.rightNum)??0,m=Math.max(x,c)-1;m>0&&b.unshift({kind:"separator",skipped:m})}return b}var Ju=M('
      '),Zu=M('
      '),ed=M(' ',1),td=M('No matches'),rd=M('
      '),nd=M('

      Files are identical.

      '),ad=M('
      Loading...
      '),od=M('
      ');function sd(t,r){Me(r,!0);let a=fe(r,"loading",3,!1),n=F(void 0),i=z(()=>Qu(r.diffText)),o=z(()=>{const te=[];return e(i).forEach((J,ae)=>{J.hunkStart&&te.push(ae)}),te}),u=z(()=>e(o).length>0),v=z(()=>{var J;const te=new Array(e(i).length).fill(-1);for(let ae=0;ae{r.diffText!==y&&(y=r.diffText,d(l,-1))});let b=z(()=>{if(!r.search.trim())return[];try{const te=new RegExp(r.search,"i"),J=[];return e(i).forEach((ae,de)=>{ae.kind==="line"&&ae.row&&(te.test(ae.row.leftContent)||te.test(ae.row.rightContent))&&J.push(de)}),J}catch{return[]}}),S=F(-1),w="";Ee(()=>{r.search!==w&&(w=r.search,d(S,e(b).length>0?0:-1,!0))});async function T(te){var de;d(S,te,!0),await St();const J=e(b)[te],ae=(de=e(_))==null?void 0:de.querySelector(`[data-item-idx="${J}"]`);if(ae&&e(_)&&e(R)){const xe=e(_).getBoundingClientRect(),pe=ae.getBoundingClientRect().top-xe.top+e(_).scrollTop-e(_).clientHeight/3;h=!0,e(_).scrollTop=pe,e(R).scrollTop=pe,requestAnimationFrame(()=>{h=!1})}}function N(){e(S)>0&&T(e(S)-1)}function x(){e(S)<0&&e(b).length>0?T(0):e(S)$1`)}catch{return ae}}function m(te,J){return te==="equal"?"":te==="del"&&J==="left"?"bg-[rgba(214,67,10,0.08)] text-edf-orange-fonce":te==="del"&&J==="right"?"bg-[rgba(48,122,16,0.08)] text-edf-vert-fonce":te==="add"&&J==="left"?"bg-edf-gris-clair":te==="add"&&J==="right"?"bg-[rgba(48,122,16,0.08)] text-edf-vert-fonce":""}function P(te,J){return te==="del"&&J==="left"?"rgb(251,237,233)":te==="del"&&J==="right"?"rgb(238,247,236)":te==="add"&&J==="left"?"var(--color-edf-gris-clair)":te==="add"&&J==="right"?"rgb(238,247,236)":"white"}let _=F(void 0),R=F(void 0),$=!1,h=!1;function E(te){$||h||!e(_)||!e(R)||($=!0,te==="left"?(e(R).scrollTop=e(_).scrollTop,e(R).scrollLeft=e(_).scrollLeft):(e(_).scrollTop=e(R).scrollTop,e(_).scrollLeft=e(R).scrollLeft),requestAnimationFrame(()=>{$=!1}))}async function D(te){var ae;d(l,te,!0),await St();const J=(ae=e(_))==null?void 0:ae.querySelector(`[data-hunk-start="${te}"]`);if(J&&e(_)&&e(R)){h=!0;const de=e(_).getBoundingClientRect(),Ae=J.getBoundingClientRect().top-de.top+e(_).scrollTop-e(_).clientHeight/3;e(_).scrollTop=Ae,e(R).scrollTop=Ae,requestAnimationFrame(()=>{h=!1})}}function L(){e(l)>0&&D(e(l)-1)}function j(){e(l)<0?D(0):e(l)=0&&d(l,J,!0)}function V(te){te.target instanceof HTMLInputElement||(te.key==="n"&&!te.shiftKey?(te.preventDefault(),j()):te.key==="N"||te.key==="n"&&te.shiftKey?(te.preventDefault(),L()):te.key==="j"?(te.preventDefault(),x()):(te.key==="J"||te.key==="j"&&te.shiftKey)&&(te.preventDefault(),N()))}var O=od();{const te=(J,ae=ba)=>{var de=Ke(),xe=Y(de);je(xe,17,()=>e(i),vn,(Ae,pe,ve)=>{const Ue=z(()=>e(o).indexOf(ve)),ze=z(()=>e(v)[ve]>=0&&e(v)[ve]===e(l)),rt=z(()=>{var lt,Ve;return ae()==="left"?(lt=e(pe).row)==null?void 0:lt.leftNum:(Ve=e(pe).row)==null?void 0:Ve.rightNum}),er=z(()=>{var lt,Ve;return ae()==="left"?((lt=e(pe).row)==null?void 0:lt.leftContent)??"":((Ve=e(pe).row)==null?void 0:Ve.rightContent)??""});var br=Ke(),Qe=Y(br);{var ut=lt=>{var Ve=Ju(),$t=A(k(Ve),2),Ir=k($t);p($t),p(Ve),Q(()=>re(Ir,`... ${e(pe).skipped??""} lines hidden ...`)),f(lt,Ve)},zr=lt=>{var Ve=Zu(),$t=k(Ve),Ir=k($t,!0);p($t);var $n=A($t,2);Tr($n,()=>c(e(er),e(S)>=0&&e(b)[e(S)]===ve),!0),p($n),p(Ve),Q((bt,Mt,xr,Ga,Xa)=>{ke(Ve,1,`flex w-full ${bt??""} ${Mt??""}`),Ge(Ve,e(ze)?"background-color: rgba(16,87,200,0.12); color: rgb(16,87,200);":""),ye(Ve,"role",xr),ye(Ve,"tabindex",Ga),ye(Ve,"data-hunk-start",ae()==="left"&&e(Ue)>=0?e(Ue):void 0),ye(Ve,"data-item-idx",ae()==="left"?ve:void 0),Ge($t,`background: ${Xa??""}; color: ${e(ze)?"white":"var(--color-muted)"};`),re(Ir,e(rt)??"")},[()=>m(e(pe).row.type,ae()),()=>e(pe).row.type!=="equal"||e(b).includes(ve)?"cursor-pointer":"",()=>e(pe).row.type!=="equal"||e(b).includes(ve)?"button":void 0,()=>e(pe).row.type!=="equal"||e(b).includes(ve)?0:void 0,()=>e(ze)?"rgb(16,87,200)":P(e(pe).row.type,ae())]),ie("click",Ve,()=>{var Mt;((Mt=e(pe).row)==null?void 0:Mt.type)!=="equal"&&I(ve);const bt=e(b).indexOf(ve);bt>=0&&d(S,bt,!0)}),ie("keydown",Ve,bt=>{var Mt;if(bt.key==="Enter"||bt.key===" "){bt.preventDefault(),((Mt=e(pe).row)==null?void 0:Mt.type)!=="equal"&&I(ve);const xr=e(b).indexOf(ve);xr>=0&&d(S,xr,!0)}}),f(lt,Ve)};G(Qe,lt=>{e(pe).kind==="separator"?lt(ut):e(pe).row&<(zr,1)})}f(Ae,br)}),f(J,de)};var B=k(O),U=k(B),q=k(U),g=k(q,!0);p(q);var s=A(q,2),C=k(s,!0);p(s),p(U);var H=A(U,2),W=k(H),ee=k(W,!0);p(W);var ne=A(W,2),me=k(ne,!0);p(ne),p(H),p(B);var K=A(B,2),X=k(K),ce=k(X);qe(ce);var le=A(ce,2);{var we=J=>{var ae=ed(),de=Y(ae);{let ve=z(()=>e(S)<=0);Re(de,{variant:"secondary",size:"sm",onclick:N,get disabled(){return e(ve)},children:(Ue,ze)=>{Ce();var rt=Ie("Prev");f(Ue,rt)},$$slots:{default:!0}})}var xe=A(de,2),Ae=k(xe);p(xe);var pe=A(xe,2);{let ve=z(()=>e(S)>=e(b).length-1);Re(pe,{variant:"secondary",size:"sm",onclick:x,get disabled(){return e(ve)},children:(Ue,ze)=>{Ce();var rt=Ie("Next");f(Ue,rt)},$$slots:{default:!0}})}Q(()=>re(Ae,`${e(S)>=0?e(S)+1:"–"} / ${e(b).length??""} occurrence${e(b).length!==1?"s":""}`)),f(J,ae)},he=z(()=>r.search.trim()&&e(b).length>0),Z=J=>{var ae=td();f(J,ae)},_e=z(()=>r.search.trim());G(le,J=>{e(he)?J(we):e(_e)&&J(Z,1)})}p(X);var Se=A(X,2),Pe=k(Se);{let J=z(()=>e(o).length===0||e(l)<=0);Re(Pe,{variant:"secondary",size:"sm",onclick:L,get disabled(){return e(J)},children:(ae,de)=>{Ce();var xe=Ie("Prev");f(ae,xe)},$$slots:{default:!0}})}var ge=A(Pe,2),De=k(ge);p(ge);var be=A(ge,2);{let J=z(()=>e(o).length===0||e(l)>=e(o).length-1);Re(be,{variant:"secondary",size:"sm",onclick:j,get disabled(){return e(J)},children:(ae,de)=>{Ce();var xe=Ie("Next");f(ae,xe)},$$slots:{default:!0}})}p(Se),p(K);var Oe=A(K,2);{var Fe=J=>{var ae=rd(),de=k(ae),xe=k(de),Ae=k(xe);te(Ae,()=>"left"),p(xe),p(de),tt(de,ze=>d(_,ze),()=>e(_));var pe=A(de,2),ve=k(pe),Ue=k(ve);te(Ue,()=>"right"),p(ve),p(pe),tt(pe,ze=>d(R,ze),()=>e(R)),p(ae),Ze("scroll",de,()=>E("left")),Ze("scroll",pe,()=>E("right")),f(J,ae)},oe=J=>{var ae=nd();f(J,ae)};G(Oe,J=>{e(u)?J(Fe):J(oe,-1)})}var ue=A(Oe,2);{var He=J=>{var ae=ad();f(J,ae)};G(ue,J=>{a()&&J(He)})}p(O),tt(O,J=>d(n,J),()=>e(n)),Q(()=>{re(g,r.leftLabel),re(C,r.kind),re(ee,r.rightLabel),re(me,r.kind),gn(ce,r.search),re(De,`${e(l)>=0?e(l)+1:"–"} / ${e(o).length??""} diff${e(o).length!==1?"s":""}`)}),ie("input",ce,J=>r.onSearchChange(J.target.value)),ie("keydown",ce,J=>{J.key==="Enter"&&(J.preventDefault(),x())})}ie("keydown",O,V),f(t,O),Ne()}Be(["keydown","click","input"]);var id=M(" ",1),ld=M('

      Please select two different cases to compare.

      '),cd=M('

      Select two cases to compare.

      '),ud=M(' '),dd=M('
      Parameter
      '),fd=M('

      All parameters are identical.

      '),vd=M('
      ',1),gd=M('

      '),hd=M('

      Loading...

      '),pd=M(" ",1),md=M(" ",1);function _d(t,r){Me(r,!0);let a=F(""),n=F(""),i=F(""),o=F(""),u=F(""),v=F(!1),l=F(!1),y=F(""),b=z(()=>r.allCases.map(h=>({value:h,label:h})));const S=[{value:"doe_row.csv",label:"doe_row.csv"}];let w=z(()=>{var h;return((h=Pt())==null?void 0:h.compare_kinds)??S});Ee(()=>{var h;e(w).some(E=>E.value===e(i))||d(i,((h=e(w)[0])==null?void 0:h.value)??"",!0)});let T=F(!1),N=z(()=>e(a)&&e(n)&&e(a)!==e(n));function x(){const h=e(a);d(a,e(n),!0),d(n,h,!0)}async function c(){if(e(N)){d(v,!0),d(y,"");try{const h=await Us({cases:[e(a),e(n)],base:e(a),kind:e(i)});d(u,h,!0),d(l,!0)}catch(h){console.error("Failed to load diff:",h),d(u,""),d(l,!1);const E=h instanceof Error?h.message:String(h);d(y,E.includes("404")?`File "${e(i)}" not found for one of the selected cases. It may not have been run yet.`:"Failed to load comparison.",!0)}d(v,!1)}}Ee(()=>{e(a)&&e(n)&&e(a)!==e(n)&&e(i)?c():e(a)&&e(n)&&e(a)===e(n)&&(d(u,""),d(l,!0))});let m=!1;Ee(()=>{r.allCases.length>=2&&!m?(m=!0,d(a,r.allCases[0],!0),d(n,r.allCases[1],!0)):r.allCases.length===1&&!m&&(m=!0,d(a,r.allCases[0],!0))});let P=z(Sn),_=z(()=>{if(!e(a)||!e(n)||e(a)===e(n)||e(P).length===0)return[];const h=Fr(),E=h.find(L=>L.case_id===e(a)),D=h.find(L=>L.case_id===e(n));return!E&&!D?[]:e(P).map(L=>{var V,O;const j=((V=E==null?void 0:E.doe)==null?void 0:V[L])!=null&&String(E.doe[L])!==""?String(E.doe[L]):"—",I=((O=D==null?void 0:D.doe)==null?void 0:O[L])!=null&&String(D.doe[L])!==""?String(D.doe[L]):"—";return{param:L,left:j,right:I,differs:j!==I}})}),R=z(()=>e(T)?e(_):e(_).filter(h=>h.differs)),$=z(()=>e(_).filter(h=>h.differs).length);Ht(t,{eyebrow:"Compare",title:"Side-by-Side Comparison",wide:!0,children:(h,E)=>{var D=md(),L=Y(D);Wt(L,{children:(B,U)=>{var q=id(),g=Y(q);$e(g,{text:"First case",children:(H,W)=>{st(H,{class:"w-32.5",get options(){return e(b)},get value(){return e(a)},onchange:ee=>d(a,ee,!0),placeholder:"Select..."})}});var s=A(g,2);{let H=z(()=>!e(a)&&!e(n));Re(s,{variant:"secondary",size:"sm",onclick:x,get disabled(){return e(H)},children:(W,ee)=>{Le(W,{get icon(){return pi}})},$$slots:{default:!0}})}var C=A(s,2);$e(C,{text:"Second case",children:(H,W)=>{st(H,{class:"w-32.5",get options(){return e(b)},get value(){return e(n)},onchange:ee=>d(n,ee,!0),placeholder:"Select..."})}}),f(B,q)}});var j=A(L,2);{var I=B=>{var U=ld();f(B,U)},V=B=>{var U=cd();f(B,U)},O=B=>{var U=pd(),q=Y(U);{var g=ne=>{var me=vd(),K=Y(me),X=k(K),ce=k(X);p(X);var le=A(X,2),we=k(le,!0);p(le),p(K);var he=A(K,2);{var Z=Se=>{var Pe=dd(),ge=k(Pe),De=k(ge),be=k(De),Oe=k(be),Fe=k(Oe,!0);p(Oe);var oe=A(Oe,2),ue=k(oe,!0);p(oe),p(be),p(De);var He=A(De);je(He,21,()=>e(R),te=>te.param,(te,J)=>{var ae=ud(),de=k(ae),xe=k(de,!0);p(de);var Ae=A(de),pe=k(Ae,!0);p(Ae);var ve=A(Ae),Ue=k(ve,!0);p(ve),p(ae),Q(()=>{ke(de,1,dt(e(J).differs?"text-edf-orange-fonce bg-[rgba(214,67,10,0.04)]":"")),re(xe,e(J).left),re(pe,e(J).param),ke(ve,1,dt(e(J).differs?"text-edf-vert-fonce bg-[rgba(48,122,16,0.04)]":"")),re(Ue,e(J).right)}),f(te,ae)}),p(He),p(ge),p(Pe),Q(()=>{re(Fe,e(a)),re(ue,e(n))}),f(Se,Pe)},_e=Se=>{var Pe=fd();f(Se,Pe)};G(he,Se=>{e(R).length>0?Se(Z):Se(_e,-1)})}Q(()=>{re(ce,`${e($)??""} difference${e($)!==1?"s":""} out of ${e(_).length??""} + parameters`),re(we,e(T)?"Show differences only":"Show all parameters")}),ie("click",le,()=>d(T,!e(T))),f(ne,me)};G(q,ne=>{e(_).length>0&&ne(g)})}var s=A(q,2);Wt(s,{children:(ne,me)=>{$e(ne,{text:"File",children:(K,X)=>{st(K,{class:"w-37.5",get options(){return e(w)},get value(){return e(i)},onchange:ce=>d(i,ce,!0)})}})}});var C=A(s,2);{var H=ne=>{var me=gd(),K=k(me,!0);p(me),Q(()=>re(K,e(y))),f(ne,me)},W=ne=>{sd(ne,{get diffText(){return e(u)},get leftLabel(){return e(a)},get rightLabel(){return e(n)},get kind(){return e(i)},get search(){return e(o)},get loading(){return e(v)},onSearchChange:me=>d(o,me,!0)})},ee=ne=>{var me=hd();f(ne,me)};G(C,ne=>{e(y)?ne(H):e(l)?ne(W,1):e(v)&&ne(ee,2)})}f(B,U)};G(j,B=>{e(a)&&e(n)&&e(a)===e(n)?B(I):!e(a)||!e(n)?B(V,1):B(O,-1)})}f(h,D)},$$slots:{default:!0}}),Ne()}Be(["click"]);var bd=M(" Refresh",1),xd=M(" ",1),yd=M(''),wd=M(''),kd=M(" Download as CSV",1),Cd=M('
      ',1),Sd=M(" ",1),Pd=M(""),Rd=M('
      Sort by:
      '),Ed=M('

      No errors found. Select cases and adjust filters above.

      '),$d=M(' '),Md=M('NEW'),Nd=M('
      '),Ad=M('
      '),Td=M(' '),Ld=M('
      '),Dd=M('

      '),Fd=M("
      ",1);function zd(t,r){Me(r,!0);const a=6,n=["csauto.stderr","csauto.stdout"];let i=F(se([])),o=F(se([...n])),u=!1;Ee(()=>{var X;const K=(X=Pt())==null?void 0:X.error_files;K!=null&&K.length&&!u&&d(o,[...K],!0)});let v=F("all"),l=F(""),y=F(a),b=F(!1),S=F(""),w=F(se(Qt("errors")));Ee(()=>{Jt("errors",e(w))});let T=F(se([])),N=F(se(new Set));function x(K){return K.toLowerCase().replace(/[0-9]+/g,"#").replace(/0x[0-9a-f]+/gi,"#").replace(/\s+/g," ").trim()}function c(K){const X=K.line_html.replace(/<[^>]*>/g,"").slice(0,120);return`${K.case_id}|${K.file}|${K.severity}|${x(X)}`}function m(K){const X=new Map;for(const ce of K){const le=c(ce),we=X.get(le);we?we.count++:X.set(le,{...ce,count:1,fingerprint:le,isNew:!e(N).has(le)})}return Array.from(X.values())}async function P(){if(!(!e(i).length||!e(o).length)){d(b,!0);try{const K=await Fs({cases:e(i),files:e(o),context:e(y),sev:e(v)==="all"?"":e(v),q:e(l)});d(S,"");const X=m(K.items);d(T,K.items,!0);const ce=new Set(X.map(le=>le.fingerprint));d(N,ce,!0),d(_,X,!0)}catch(K){d(S,"Failed to load errors"),console.error("Failed to load errors:",K)}finally{d(b,!1)}}}let _=F(se([])),R=null;function $(){R&&clearTimeout(R),R=setTimeout(P,250)}function h(K){return K==="error"?"error-sev-error":K==="warn"?"error-sev-warn":"error-sev-info"}let E=z(()=>xn(r.allCases)),D=z(()=>{var K;return(((K=Pt())==null?void 0:K.error_files)??n).map(X=>({value:X,label:X}))}),L=[{value:"all",label:"All"},{value:"error",label:"Error"},{value:"warn",label:"Warn"}];function j(K){if(d(i,K,!0),K.length===0){d(_,[],!0),d(T,[],!0);return}P()}function I(K){if(u=!0,d(o,K,!0),K.length===0){d(_,[],!0),d(T,[],!0);return}P()}function V(K){d(v,K,!0),P()}function O(){P()}let B=!1;Ee(()=>{r.allCases.length>0&&!B&&(B=!0,d(i,[...r.allCases],!0),P())}),Ee(()=>{e(w)?hr("errors",P,Wn()):ft("errors")});const U=pr(()=>{B&&P()});dr(()=>{ft("errors"),U()});let q=F(se(new Set));function g(K){const X=new Set(e(q));X.has(K)?X.delete(K):X.add(K),d(q,X,!0)}let s=F("severity"),C=F("desc");const H={info:0,warn:1,error:2};let W=z(()=>{const K=[...e(_)];return K.sort((X,ce)=>{let le=0;return e(s)==="severity"?le=(H[X.severity]??9)-(H[ce.severity]??9):e(s)==="count"?le=X.count-ce.count:le=X[e(s)].localeCompare(ce[e(s)]),e(C)==="asc"?le:-le}),K});const ee={severity:"desc",count:"desc",case_id:"asc",file:"asc"};function ne(K){e(s)===K?d(C,e(C)==="asc"?"desc":"asc",!0):(d(s,K,!0),d(C,ee[K],!0))}function me(){if(!e(_).length)return;const K=["case_id","file","severity","count","is_new","line_text"],X=e(W).map(we=>[we.case_id,we.file,we.severity,we.count,we.isNew?"yes":"no",`"${we.line_html.replace(/<[^>]*>/g,"").replace(/"/g,'""')}"`].join(",")),ce=[K.join(","),...X].join(` +`),le=_r("errors",e(i),"csv");za(ce,le)}Ht(t,{eyebrow:"Diagnostics",title:"Recent Errors",wide:!0,actions:X=>{var ce=xd(),le=Y(ce);{let Z=z(Wn);Zt(le,{name:"errors",get intervalMs(){return e(Z)},onRefresh:P,get checked(){return e(w)},set checked(_e){d(w,_e,!0)}})}var we=A(le,2);{var he=Z=>{Re(Z,{variant:"primary",onclick:P,children:(_e,Se)=>{var Pe=bd(),ge=Y(Pe);Le(ge,{get icon(){return Yt}}),Ce(),f(_e,Pe)},$$slots:{default:!0}})};G(we,Z=>{e(w)||Z(he)})}f(X,ce)},children:(X,ce)=>{var le=Fd(),we=Y(le);Wt(we,{children:(oe,ue)=>{var He=Cd(),te=Y(He);$e(te,{text:"Cases",children:(ve,Ue)=>{Bt(ve,{class:"w-[160px]",get options(){return e(E)},get selected(){return e(i)},onchange:j,placeholder:"Select cases..."})}});var J=A(te,2);$e(J,{text:"Files",children:(ve,Ue)=>{Bt(ve,{class:"w-[160px]",get options(){return e(D)},get selected(){return e(o)},onchange:I,placeholder:"Select files..."})}});var ae=A(J,2);$e(ae,{text:"Severity",children:(ve,Ue)=>{st(ve,{class:"w-[90px]",get options(){return L},get value(){return e(v)},onchange:V})}});var de=A(ae,2);$e(de,{text:"Search",children:(ve,Ue)=>{var ze=yd();qe(ze),ie("input",ze,$),Je(ze,()=>e(l),rt=>d(l,rt)),f(ve,ze)}});var xe=A(de,2);$e(xe,{text:"Context",children:(ve,Ue)=>{var ze=wd();qe(ze),ie("change",ze,O),Je(ze,()=>e(y),rt=>d(y,rt)),f(ve,ze)}});var Ae=A(xe,2),pe=k(Ae);{let ve=z(()=>e(_).length===0);Re(pe,{variant:"secondary",size:"sm",onclick:me,get disabled(){return e(ve)},children:(Ue,ze)=>{var rt=kd(),er=Y(rt);Le(er,{get icon(){return gr}}),Ce(),f(Ue,rt)},$$slots:{default:!0}})}p(Ae),f(oe,He)}});var he=A(we,2);{var Z=oe=>{var ue=Rd(),He=A(k(ue),2);je(He,16,()=>[{key:"severity",label:"Severity"},{key:"count",label:"Count"},{key:"case_id",label:"Case"},{key:"file",label:"File"}],te=>te.key,(te,J)=>{var ae=Pd(),de=k(ae,!0),xe=A(de);{var Ae=pe=>{var ve=Sd(),Ue=A(Y(ve));{let ze=z(()=>e(C)==="asc"?mi:hi);Le(Ue,{get icon(){return e(ze)},size:12})}f(pe,ve)};G(xe,pe=>{e(s)===J.key&&pe(Ae)})}p(ae),Q(()=>{ke(ae,1,`cursor-pointer bg-transparent border-none text-xs hover:underline ${e(s)===J.key?"text-edf-bleu-fonce font-bold":"text-muted"}`),re(de,J.label)}),ie("click",ae,()=>ne(J.key)),f(te,ae)}),p(ue),f(oe,ue)};G(he,oe=>{e(W).length>0&&oe(Z)})}var _e=A(he,2),Se=k(_e);{var Pe=oe=>{var ue=Ed();f(oe,ue)};G(Se,oe=>{e(W).length===0&&oe(Pe)})}var ge=A(Se,2);je(ge,17,()=>e(W),oe=>oe.fingerprint,(oe,ue)=>{var He=Ad(),te=k(He),J=k(te),ae=k(J);{let Qe=z(()=>e(q).has(e(ue).fingerprint)?yn:_i);Le(ae,{get icon(){return e(Qe)},size:14})}p(J);var de=A(J,2),xe=k(de);p(de);var Ae=A(de,2),pe=k(Ae);{var ve=Qe=>{var ut=$d(),zr=k(ut);p(ut),Q(()=>re(zr,`${e(ue).count??""}x`)),f(Qe,ut)};G(pe,Qe=>{e(ue).count>1&&Qe(ve)})}var Ue=A(pe,2);{var ze=Qe=>{var ut=Md();f(Qe,ut)};G(Ue,Qe=>{e(ue).isNew&&Qe(ze)})}p(Ae),p(te);var rt=A(te,2);{var er=Qe=>{var ut=Nd();Tr(ut,()=>e(ue).line_html,!0),p(ut),f(Qe,ut)},br=z(()=>e(q).has(e(ue).fingerprint));G(rt,Qe=>{e(br)&&Qe(er)})}p(He),Q(Qe=>{ke(He,1,`error-item ${Qe??""}`),re(xe,`${e(ue).case_id??""} / ${e(ue).file??""} : ${e(ue).severity??""}`)},[()=>h(e(ue).severity)]),ie("click",te,()=>g(e(ue).fingerprint)),f(oe,He)}),p(_e);var De=A(_e,2);{var be=oe=>{var ue=Ld(),He=k(ue),te=k(He);p(He);var J=A(He,2);{var ae=xe=>{var Ae=Td(),pe=k(Ae);p(Ae),Q(ve=>re(pe,`${ve??""} new`),[()=>e(_).filter(ve=>ve.isNew).length]),f(xe,Ae)},de=z(()=>e(_).some(xe=>xe.isNew));G(J,xe=>{e(de)&&xe(ae)})}p(ue),Q(()=>re(te,`${e(_).length??""} unique / ${e(T).length??""} total`)),f(oe,ue)};G(De,oe=>{e(W).length>0&&oe(be)})}var Oe=A(De,2);{var Fe=oe=>{var ue=Dd(),He=k(ue,!0);p(ue),Q(()=>re(He,e(S))),f(oe,ue)};G(Oe,oe=>{e(S)&&oe(Fe)})}Q(()=>ke(_e,1,`error-list ${e(b)?"opacity-50 pointer-events-none":""}`)),f(X,le)},$$slots:{actions:!0,default:!0}}),Ne()}Be(["input","change","click"]);var Id=M('
      ',1);function Kd(t,r){Me(r,!0);let a=F(se({totalCases:0,shownCases:0,totalRunning:0,shownRunning:0,totalConverged:0,shownConverged:0})),n=F(se([]));Ee(()=>{mn(),fn(()=>void $a())}),Ee(()=>{const s=Pt(),C=document.querySelector('link[rel="icon"]');C&&(s!=null&&s.icon)&&(C.href=Ts(s.solver))});async function i(){try{const s=await Ms();Fl(s.rows),zl(s.doe_columns),d(n,s.rows.map(C=>C.case_id),!0),d(a,Yl(Da(),Fr()),!0)}catch(s){console.error("Failed to load status:",s)}}un(()=>{i()});var o=Id(),u=Y(o);Vi(u,{});var v=A(u,2);Gi(v,{});var l=A(v,2);hl(l,{get totalCases(){return e(a).totalCases},get shownCases(){return e(a).shownCases},get totalRunning(){return e(a).totalRunning},get shownRunning(){return e(a).shownRunning},get totalConverged(){return e(a).totalConverged},get shownConverged(){return e(a).shownConverged}});var y=A(l,2),b=k(y);{var S=s=>{Pc(s,{onRefresh:i})},w=z(()=>Nt("status"));G(b,s=>{e(w)&&s(S)})}var T=A(b,2);{var N=s=>{Gc(s,{get allCases(){return e(n)}})},x=z(()=>Nt("residuals"));G(T,s=>{e(x)&&s(N)})}var c=A(T,2);{var m=s=>{_u(s,{get allCases(){return e(n)}})},P=z(()=>Nt("probes"));G(c,s=>{e(P)&&s(m)})}var _=A(c,2);{var R=s=>{$u(s,{get allCases(){return e(n)}})},$=z(()=>Nt("performance"));G(_,s=>{e($)&&s(R)})}var h=A(_,2);{var E=s=>{_d(s,{get allCases(){return e(n)}})},D=z(()=>Nt("compare"));G(h,s=>{e(D)&&s(E)})}var L=A(h,2);{var j=s=>{Yu(s,{get allCases(){return e(n)}})},I=z(()=>Nt("tail"));G(L,s=>{e(I)&&s(j)})}var V=A(L,2);{var O=s=>{zd(s,{get allCases(){return e(n)}})},B=z(()=>Nt("errors"));G(V,s=>{e(B)&&s(O)})}p(y);var U=A(y,2),q=A(k(U),2),g=k(q);p(q),p(U),Q(()=>ye(g,"src",pl)),f(t,o),Ne()}export{Kd as component}; diff --git a/frontend/dist/_app/immutable/nodes/2.DQxkgq4x.js b/frontend/dist/_app/immutable/nodes/2.DQxkgq4x.js deleted file mode 100644 index 763b315..0000000 --- a/frontend/dist/_app/immutable/nodes/2.DQxkgq4x.js +++ /dev/null @@ -1,1190 +0,0 @@ -import{b as Pr,a as u,f as S,c as be,t as Ve,d as Ot}from"../chunks/C14P8jaG.js";import{o as Zn,a as vr}from"../chunks/Cz6m6K8p.js";import{h as De,L as $t,G as Lt,c as Er,J as Kt,b as Jn,o as e,N as ja,ag as Va,O as En,P as zt,f as _t,ay as ea,aF as Ua,ao as Rn,e as mt,a as dr,aM as Ba,s as qa,a5 as Ka,aN as ta,aE as dn,aO as Ga,a7 as Nn,aP as Wa,aQ as Xa,aI as Ya,aR as ht,aS as ra,a1 as Qa,r as na,p as aa,aT as Ur,ae as sa,aU as Za,aV as Ja,aC as es,m as ts,d as Nr,az as oa,y as J,F as ia,aW as rs,aG as ns,aA as as,D as la,aX as ca,aY as ss,aZ as os,E as is,aw as ua,S as Dr,k as Mr,a_ as da,a$ as ls,b0 as cs,b1 as fa,b2 as us,b3 as ds,b4 as gr,b5 as fs,b6 as vs,b7 as gs,b8 as hs,b9 as ps,ba as ms,bb as _s,aJ as Pt,U as bs,aK as z,_ as ie,$ as f,A as k,B as p,x as Me,g as K,z as Ae,C as $,j as Re,ap as ke,aL as j,bc as xs,q as Vt,bd as ys,be as it,bf as ws}from"../chunks/VJhVTb1A.js";import{p as me,i as Q,b as nt,c as ks,l as Oe,s as Ue}from"../chunks/zf4GFcxs.js";import{i as Cs,a as Ss,d as le,b as Ke,c as $s,n as Ps,e as Es,s as ne,f as tt}from"../chunks/BeON-0X8.js";import{s as ft}from"../chunks/CiUbu-1F.js";import{i as Rs}from"../chunks/DmjnTBOd.js";import{B as Ns}from"../chunks/U5yjBaoU.js";function fn(t,r){return r}function Ms(t,r,a){for(var n=[],s=r.length,i,o=r.length,l=0;l{if(i){if(i.pending.delete(A),i.done.add(A),i.pending.size===0){var m=t.outrogroups;Qr(t,dn(i.done)),m.delete(i),m.size===0&&(t.outrogroups=null)}}else o-=1},!1)}if(o===0){var d=n.length===0&&a!==null;if(d){var b=a,x=b.parentNode;es(x),x.append(b),t.items.clear()}Qr(t,r,!d)}else i={pending:new Set(r),done:new Set},(t.outrogroups??(t.outrogroups=new Set)).add(i)}function Qr(t,r,a=!0){var n;if(t.pending.size>0){n=new Set;for(const o of t.pending.values())for(const l of o)n.add(t.items.get(l).e)}for(var s=0;s{var h=a();return ta(h)?h:h==null?[]:dn(h)}),m,N=new Map,L=!0;function g(h){(P.effect.f&Qa)===0&&(P.pending.delete(h),P.fallback=x,As(P,m,o,r,n),x!==null&&(m.length===0?(x.f&ht)===0?na(x):(x.f^=ht,lr(x,null,o)):aa(x,()=>{x=null})))}function c(h){P.pending.delete(h)}var w=Jn(()=>{m=e(A);var h=m.length;let y=!1;if(De){var C=ja(o)===Va;C!==(h===0)&&(o=En(),$t(o),zt(!1),y=!0)}for(var _=new Set,M=mt,F=qa(),T=0;Ti(o)):(x=dr(()=>i(Mn??(Mn=Er()))),x.f|=ht)),h>_.size&&Ba(),De&&h>0&&$t(En()),!L)if(N.set(M,_),F){for(const[V,G]of l)_.has(V)||M.skip_effect(G.e);M.oncommit(g),M.ondiscard(c)}else g(M);y&&zt(!0),e(A)}),P={effect:w,items:l,pending:N,outrogroups:null,fallback:x};L=!1,De&&(o=_t)}function ar(t){for(;t!==null&&(t.f&Za)===0;)t=t.next;return t}function As(t,r,a,n,s){var q,I,O,V,G,Z,ae,E,R;var i=(n&Ja)!==0,o=r.length,l=t.items,d=ar(t.effect.first),b,x=null,A,m=[],N=[],L,g,c,w;if(i)for(w=0;w0){var T=(n&ra)!==0&&o===0?a:null;if(i){for(w=0;w{var H,D;if(A!==void 0)for(c of A)(D=(H=c.nodes)==null?void 0:H.a)==null||D.apply()})}function Ls(t,r,a,n,s,i,o,l){var d=(o&Wa)!==0?(o&Xa)===0?Ya(a,!1,!1):Nn(a):null,b=(o&Ga)!==0?Nn(s):null;return{v:d,i:b,e:dr(()=>(i(r,d??a,b??s,l),()=>{t.delete(n)}))}}function lr(t,r,a){if(t.nodes)for(var n=t.nodes.start,s=t.nodes.end,i=r&&(r.f&ht)===0?r.nodes.start:a;n!==null;){var o=oa(n);if(i.before(n),n===s)return;n=o}}function wt(t,r,a){r===null?t.effect.first=a:r.next=a,a===null?t.effect.last=r:a.prev=r}function zr(t,r,a=!1,n=!1,s=!1,i=!1){var o=t,l="";if(a){var d=t;De&&(o=$t(Lt(d)))}J(()=>{var b=ia;if(l===(l=r()??"")){De&&Kt();return}if(a&&!De){b.nodes=null,d.innerHTML=l,l!==""&&Pr(Lt(d),d.lastChild);return}if(b.nodes!==null&&(rs(b.nodes.start,b.nodes.end),b.nodes=null),l!==""){if(De){_t.data;for(var x=Kt(),A=x;x!==null&&(x.nodeType!==ea||x.data!=="");)A=x,x=oa(x);if(x===null)throw ns(),as;Pr(_t,A),o=$t(x);return}var m=n?ca:s?ss:void 0,N=la(n?"svg":s?"math":"template",m);N.innerHTML=l;var L=n||s?N:N.content;if(Pr(Lt(L),L.lastChild),n||s)for(;Lt(L);)o.before(Lt(L));else o.before(L)}})}function He(t,r,a,n,s){var l;De&&Kt();var i=(l=r.$$slots)==null?void 0:l[a],o=!1;i===!0&&(i=r.children,o=!0),i===void 0||i(t,o?()=>n:n)}function Ts(t,r,a,n,s,i){let o=De;De&&Kt();var l=null;De&&_t.nodeType===os&&(l=_t,Kt());var d=De?_t:t,b=new Ns(d,!1);Jn(()=>{const x=r()||null;var A=ca;if(x===null){b.ensure(null,null);return}return b.ensure(x,m=>{if(x){if(l=De?l:la(x,A),Pr(l,l),n){De&&Cs(x)&&l.append(document.createComment(""));var N=De?Lt(l):l.appendChild(Er());De&&(N===null?zt(!1):$t(N)),n(l,N)}ia.nodes.end=l,m.before(l)}De&&$t(m)}),()=>{}},is),ua(()=>{}),o&&(zt(!0),$t(d))}function Fs(t,r,a){Dr(()=>{var n=Mr(()=>r(t,a==null?void 0:a())||{});if(n!=null&&n.destroy)return()=>n.destroy()})}function Ds(t,r){var a=void 0,n;da(()=>{a!==(a=r())&&(n&&(Nr(n),n=null),a&&(n=dr(()=>{Dr(()=>a(t))})))})}function va(t){var r,a,n="";if(typeof t=="string"||typeof t=="number")n+=t;else if(typeof t=="object")if(Array.isArray(t)){var s=t.length;for(r=0;r=0;){var l=o+i;(o===0||An.includes(n[o-1]))&&(l===n.length||An.includes(n[l]))?n=(o===0?"":n.substring(0,o))+n.substring(l+1):o=l}}return n===""?null:n}function Ln(t,r=!1){var a=r?" !important;":";",n="";for(var s of Object.keys(t)){var i=t[s];i!=null&&i!==""&&(n+=" "+s+": "+i+a)}return n}function Br(t){return t[0]!=="-"||t[1]!=="-"?t.toLowerCase():t}function Os(t,r){if(r){var a="",n,s;if(Array.isArray(r)?(n=r[0],s=r[1]):n=r,t){t=String(t).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var i=!1,o=0,l=!1,d=[];n&&d.push(...Object.keys(n).map(Br)),s&&d.push(...Object.keys(s).map(Br));var b=0,x=-1;const g=t.length;for(var A=0;A{Ar(t,t.__value)});r.observe(t,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),ua(()=>{r.disconnect()})}function Hs(t,r,a=r){var n=new WeakSet,s=!0;fa(t,"change",i=>{var o=i?"[selected]":":checked",l;if(t.multiple)l=[].map.call(t.querySelectorAll(o),ur);else{var d=t.querySelector(o)??t.querySelector("option:not([disabled])");l=d&&ur(d)}a(l),t.__value=l,mt!==null&&n.add(mt)}),Dr(()=>{var i=r();if(t===document.activeElement){var o=mt;if(n.has(o))return}if(Ar(t,i,s),s&&i===void 0){var l=t.querySelector(":checked");l!==null&&(i=ur(l),a(i))}t.__value=i,s=!1}),ga(t)}function ur(t){return"__value"in t?t.__value:t.value}const sr=Symbol("class"),or=Symbol("style"),ha=Symbol("is custom element"),pa=Symbol("is html"),js=gr?"link":"LINK",Vs=gr?"input":"INPUT",Us=gr?"option":"OPTION",Bs=gr?"select":"SELECT",qs=gr?"progress":"PROGRESS";function Xe(t){if(De){var r=!1,a=()=>{if(!r){if(r=!0,t.hasAttribute("value")){var n=t.value;we(t,"value",null),t.value=n}if(t.hasAttribute("checked")){var s=t.checked;we(t,"checked",null),t.checked=s}}};t.__on_r=a,sa(a),gs()}}function vn(t,r){var a=gn(t);a.value===(a.value=r??void 0)||t.value===r&&(r!==0||t.nodeName!==qs)||(t.value=r??"")}function Ks(t,r){r?t.hasAttribute("selected")||t.setAttribute("selected",""):t.removeAttribute("selected")}function we(t,r,a,n){var s=gn(t);De&&(s[r]=t.getAttribute(r),r==="src"||r==="srcset"||r==="href"&&t.nodeName===js)||s[r]!==(s[r]=a)&&(r==="loading"&&(t[ms]=a),a==null?t.removeAttribute(r):typeof a!="string"&&ma(t).includes(r)?t[r]=a:t.setAttribute(r,a))}function Gs(t,r,a,n,s=!1,i=!1){if(De&&s&&t.nodeName===Vs){var o=t,l=o.type==="checkbox"?"defaultChecked":"defaultValue";l in a||Xe(o)}var d=gn(t),b=d[ha],x=!d[pa];let A=De&&b;A&&zt(!1);var m=r||{},N=t.nodeName===Us;for(var L in r)L in a||(a[L]=null);a.class?a.class=dt(a.class):a[sr]&&(a.class=null),a[or]&&(a.style??(a.style=null));var g=ma(t);for(const _ in a){let M=a[_];if(N&&_==="value"&&M==null){t.value=t.__value="",m[_]=M;continue}if(_==="class"){var c=t.namespaceURI==="http://www.w3.org/1999/xhtml";Se(t,c,M,n,r==null?void 0:r[sr],a[sr]),m[_]=M,m[sr]=a[sr];continue}if(_==="style"){Ze(t,M,r==null?void 0:r[or],a[or]),m[_]=M,m[or]=a[or];continue}var w=m[_];if(!(M===w&&!(M===void 0&&t.hasAttribute(_)))){m[_]=M;var P=_[0]+_[1];if(P!=="$$")if(P==="on"){const F={},T="$$"+_;let q=_.slice(2);var h=Es(q);if(Ss(q)&&(q=q.slice(0,-7),F.capture=!0),!h&&w){if(M!=null)continue;t.removeEventListener(q,m[T],F),m[T]=null}if(h)le(q,t,M),Ke([q]);else if(M!=null){let I=function(O){m[_].call(this,O)};m[T]=$s(q,t,I,F)}}else if(_==="style")we(t,_,M);else if(_==="autofocus")fs(t,!!M);else if(!b&&(_==="__value"||_==="value"&&M!=null))t.value=t.__value=M;else if(_==="selected"&&N)Ks(t,M);else{var y=_;x||(y=Ps(y));var C=y==="defaultValue"||y==="defaultChecked";if(M==null&&!b&&!C)if(d[_]=null,y==="value"||y==="checked"){let F=t;const T=r===void 0;if(y==="value"){let q=F.defaultValue;F.removeAttribute(y),F.defaultValue=q,F.value=F.__value=T?q:null}else{let q=F.defaultChecked;F.removeAttribute(y),F.defaultChecked=q,F.checked=T?q:!1}}else t.removeAttribute(_);else C||g.includes(y)&&(b||typeof M!="string")?(t[y]=M,y in d&&(d[y]=vs)):typeof M!="function"&&we(t,y,M)}}}return A&&zt(!0),m}function Tn(t,r,a=[],n=[],s=[],i,o=!1,l=!1){us(s,a,n,d=>{var b=void 0,x={},A=t.nodeName===Bs,m=!1;if(da(()=>{var L=r(...d.map(e)),g=Gs(t,b,L,i,o,l);m&&A&&"value"in L&&Ar(t,L.value);for(let w of Object.getOwnPropertySymbols(x))L[w]||Nr(x[w]);for(let w of Object.getOwnPropertySymbols(L)){var c=L[w];w.description===ds&&(!b||c!==b[w])&&(x[w]&&Nr(x[w]),x[w]=dr(()=>Ds(t,()=>c))),g[w]=c}b=g}),A){var N=t;Dr(()=>{Ar(N,b.value,!0),ga(N)})}m=!0})}function gn(t){return t.__attributes??(t.__attributes={[ha]:t.nodeName.includes("-"),[pa]:t.namespaceURI===hs})}var Fn=new Map;function ma(t){var r=t.getAttribute("is")||t.nodeName,a=Fn.get(r);if(a)return a;Fn.set(r,a=[]);for(var n,s=t,i=Element.prototype;i!==s;){n=_s(s);for(var o in n)n[o].set&&a.push(o);s=ps(s)}return a}function et(t,r,a=r){var n=new WeakSet;fa(t,"input",async s=>{var i=s?t.defaultValue:t.value;if(i=Kr(t)?Gr(i):i,a(i),mt!==null&&n.add(mt),await Pt(),i!==(i=r())){var o=t.selectionStart,l=t.selectionEnd,d=t.value.length;if(t.value=i??"",l!==null){var b=t.value.length;o===l&&l===d&&b>d?(t.selectionStart=b,t.selectionEnd=b):(t.selectionStart=o,t.selectionEnd=Math.min(l,b))}}}),(De&&t.defaultValue!==t.value||Mr(r)==null&&t.value)&&(a(Kr(t)?Gr(t.value):t.value),mt!==null&&n.add(mt)),bs(()=>{var s=r();if(t===document.activeElement){var i=mt;if(n.has(i))return}Kr(t)&&s===Gr(t.value)||t.type==="date"&&!s&&!t.value||s!==t.value&&(t.value=s??"")})}function Kr(t){var r=t.type;return r==="number"||r==="range"}function Gr(t){return t===""?null:+t}let xt=z(ie({type:"none",props:{},resolve:null}));function Ws(){return e(xt)}function ot(t=void 0){const{resolve:r}=e(xt);f(xt,{type:"none",props:{},resolve:null},!0),r==null||r(t)}function Xs(t){return new Promise(r=>{f(xt,{type:"run",props:{cases:t},resolve:r},!0)})}function Ys(t){return new Promise(r=>{f(xt,{type:"restart",props:{cases:t},resolve:r},!0)})}function Qs(t){return new Promise(r=>{f(xt,{type:"clean",props:{cases:t},resolve:r},!0)})}function We(t,r="Alert"){return new Promise(a=>{f(xt,{type:"alert",props:{title:r,message:t},resolve:()=>a()},!0)})}function Dn(t,r="Confirm",a="OK",n="primary"){return new Promise(s=>{f(xt,{type:"confirm",props:{title:r,message:t,confirmLabel:a,confirmVariant:n},resolve:s},!0)})}function hn(t,r="",a="Input",n="",s=!1){return new Promise(i=>{f(xt,{type:"prompt",props:{title:a,message:t,value:r,placeholder:n,multiline:s},resolve:i},!0)})}var Zs=S('
      ');function Jt(t,r){function a(i){return document.body.appendChild(i),{destroy(){i.remove()}}}var n=Zs(),s=k(n);ft(s,()=>r.children),p(n),Fs(n,i=>a==null?void 0:a(i)),u(t,n)}var Js=S('
      '),eo=S('
      ');function hr(t,r){Me(r,!0);const a=L=>{var g=eo(),c=k(g),w=k(c),P=k(w),h=k(P,!0);p(P);var y=$(P,2);{var C=T=>{var q=Js(),I=k(q,!0);p(q),J(()=>ne(I,r.subtitle)),u(T,q)};Q(y,T=>{r.subtitle&&T(C)})}p(w);var _=$(w,2);ft(_,()=>r.children);var M=$(_,2),F=k(M);ft(F,()=>r.footer),p(M),p(c),p(g),J(()=>{Se(c,1,`w-[min(${n()??""},96vw)] bg-white border border-border rounded-[10px] p-4.5`),we(c,"aria-labelledby",r.titleId),we(P,"id",r.titleId),ne(h,r.title)}),le("keydown",g,o),le("mousedown",g,d),le("click",g,b),u(L,g)};let n=me(r,"maxWidth",3,"520px"),s=me(r,"portal",3,!1);function i(){r.onCancel?r.onCancel():ot(null)}function o(L){L.key==="Escape"?i():L.key==="Enter"&&r.onConfirm&&r.onConfirm()}let l=!1;function d(L){l=L.target.dataset.backdrop!==void 0}function b(L){const g=L.target.dataset.backdrop!==void 0;l&&g&&i(),l=!1}var x=be(),A=K(x);{var m=L=>{Jt(L,{children:(g,c)=>{a(g)}})},N=L=>{a(L)};Q(A,L=>{s()?L(m):L(N,-1)})}u(t,x),Ae()}Ke(["keydown","mousedown","click"]);var to=S("");function $e(t,r){let a=me(r,"variant",3,"primary"),n=me(r,"size",3,"default"),s=me(r,"disabled",3,!1);const i={default:"h-[34px] rounded-md px-3.5 leading-none text-[13px] tracking-wide",sm:"h-[30px] rounded-md px-2.5 leading-none text-xs"},o={primary:"bg-edf-bleu-moyen text-white border-none hover:bg-[rgb(12,72,170)]",secondary:"bg-white text-ink border border-border hover:bg-edf-gris-clair",run:"bg-edf-vert-fonce text-white border-none hover:bg-[rgb(38,98,12)]",warning:"bg-edf-orange-moyen text-white border-none hover:bg-[rgb(230,118,20)]",danger:"bg-edf-orange-fonce text-white border-none hover:bg-[rgb(180,56,8)]"};var l=to(),d=k(l);ft(d,()=>r.children),p(l),J(()=>{Se(l,1,`inline-flex items-center justify-center gap-1.5 font-bold cursor-pointer transition-colors duration-150 disabled:opacity-40 disabled:pointer-events-none ${i[n()]??""} ${o[a()]??""}`),l.disabled=s()}),le("click",l,function(...b){var x;(x=r.onclick)==null||x.apply(this,b)}),u(t,l)}Ke(["click"]);var ro=S(" ",1),no=S(''),ao=S(''),so=S('
      '),oo=S('

      ',1);function io(t,r){Me(r,!0);let a=me(r,"confirmLabel",3,"OK"),n=me(r,"confirmVariant",3,"primary"),s=me(r,"value",3,""),i=me(r,"placeholder",3,""),o=me(r,"multiline",3,!1);const l=s();let d=z(ie(l)),b=z(void 0),x=z(void 0);Re(()=>{var N;r.mode==="prompt"&&e(b)?(e(b).focus(),"select"in e(b)&&e(b).select()):e(x)&&((N=e(x).querySelector("button"))==null||N.focus())});function A(){r.mode==="prompt"?ot(e(d)):r.mode==="confirm"?ot(!0):ot()}function m(){r.mode==="confirm"?ot(!1):r.mode==="prompt"?ot(null):ot()}{const N=g=>{var c=ro(),w=K(c);{var P=C=>{$e(C,{variant:"secondary",onclick:m,children:(_,M)=>{ke();var F=Ve("Cancel");u(_,F)},$$slots:{default:!0}})};Q(w,C=>{r.mode!=="alert"&&C(P)})}var h=$(w,2),y=k(h);$e(y,{get variant(){return n()},onclick:A,children:(C,_)=>{ke();var M=Ve();J(()=>ne(M,a())),u(C,M)},$$slots:{default:!0}}),p(h),nt(h,C=>f(x,C),()=>e(x)),u(g,c)};let L=j(()=>o()?void 0:A);hr(t,{get title(){return r.title},titleId:"app-dialog-title",get onConfirm(){return e(L)},onCancel:m,footer:N,children:(g,c)=>{var w=oo(),P=K(w),h=k(P,!0);p(P);var y=$(P,2);{var C=_=>{var M=so(),F=k(M);{var T=I=>{var O=no();xs(O),nt(O,V=>f(b,V),()=>e(b)),J(()=>we(O,"placeholder",i())),et(O,()=>e(d),V=>f(d,V)),u(I,O)},q=I=>{var O=ao();Xe(O),nt(O,V=>f(b,V),()=>e(b)),J(()=>we(O,"placeholder",i())),et(O,()=>e(d),V=>f(d,V)),u(I,O)};Q(F,I=>{o()?I(T):I(q,-1)})}p(M),u(_,M)};Q(y,_=>{r.mode==="prompt"&&_(C)})}J(()=>ne(h,r.message)),u(g,w)},$$slots:{footer:!0,default:!0}})}Ae()}var lo=S('');function Ne(t,r){var a=lo(),n=k(a),s=$(n);ft(s,()=>r.children),p(a),J(()=>ne(n,`${r.text??""} `)),u(t,a)}function Ct(t,r){const a=localStorage.getItem(t);if(a===null)return r;const n=Number(a);return Number.isFinite(n)?n:r}function St(t,r){Number.isFinite(r)&&localStorage.setItem(t,String(r))}function co(t,r){return localStorage.getItem(t)??r}function uo(){return{n:Ct("csauto_run_n",1),nt:Ct("csauto_run_nt",1),maxParallel:Ct("csauto_run_max_parallel",0)||null}}function fo(t){St("csauto_run_n",t.n),St("csauto_run_nt",t.nt),t.maxParallel&&St("csauto_run_max_parallel",t.maxParallel)}function vo(){return{n:Ct("csauto_restart_n",1),nt:Ct("csauto_restart_nt",1),maxParallel:Ct("csauto_restart_max_parallel",0)||null,mode:co("csauto_restart_mode","iterations"),value:Ct("csauto_restart_value",100)}}function go(t){St("csauto_restart_n",t.n),St("csauto_restart_nt",t.nt),t.maxParallel&&St("csauto_restart_max_parallel",t.maxParallel),localStorage.setItem("csauto_restart_mode",t.mode),St("csauto_restart_value",t.value)}function ho(){return{keepLast:Ct("csauto_clean_keep_last",1)}}function po(t){St("csauto_clean_keep_last",t.keepLast)}var mo=S(" ",1),_o=S(''),bo=S(''),xo=S(''),yo=S('
      ');function wo(t,r){Me(r,!0);const a=uo(),n=a.maxParallel??(r.cases.length>1?r.cases.length:0);let s=z(ie(a.n)),i=z(ie(a.nt)),o=z(ie(n));async function l(){if(!Number.isFinite(e(s))||e(s)<=0){await We("MPI ranks must be an integer > 0.","Invalid value");return}if(!Number.isFinite(e(i))||e(i)<=0){await We("Threads must be an integer > 0.","Invalid value");return}if(e(o)&&(!Number.isFinite(e(o))||e(o)<=0)){await We("Max parallel must be empty or > 0.","Invalid value");return}const d={n:e(s),nt:e(i),maxParallel:e(o)||null};fo(d),ot(d)}{const d=A=>{var m=mo(),N=K(m);$e(N,{variant:"secondary",onclick:()=>ot(null),children:(g,c)=>{ke();var w=Ve("Cancel");u(g,w)},$$slots:{default:!0}});var L=$(N,2);$e(L,{variant:"run",onclick:l,children:(g,c)=>{ke();var w=Ve("Run");u(g,w)},$$slots:{default:!0}}),u(A,m)};let b=j(()=>r.cases.length),x=j(()=>r.cases.length>1?"s":"");hr(t,{title:"Run Cases",titleId:"run-dialog-title",get subtitle(){return`${e(b)??""} case${e(x)??""} selected`},onConfirm:l,footer:d,children:(A,m)=>{var N=yo(),L=k(N);Ne(L,{text:"MPI Ranks (n)",children:(w,P)=>{var h=_o();Xe(h),et(h,()=>e(s),y=>f(s,y)),u(w,h)}});var g=$(L,2);Ne(g,{text:"OMP Threads (nt)",children:(w,P)=>{var h=bo();Xe(h),et(h,()=>e(i),y=>f(i,y)),u(w,h)}});var c=$(g,2);Ne(c,{text:"Max Parallel",children:(w,P)=>{var h=xo();Xe(h),et(h,()=>e(o),y=>f(o,y)),u(w,h)}}),p(N),u(A,N)},$$slots:{footer:!0,default:!0}})}Ae()}var ko=S(" ",1),Co=S(''),So=S(''),$o=S(''),Po=S(""),Eo=S(''),Ro=S('
      ',1);function No(t,r){Me(r,!0);const a=vo(),n=a.maxParallel??(r.cases.length>1?r.cases.length:0);let s=z(ie(a.n)),i=z(ie(a.nt)),o=z(ie(n)),l=z(ie(a.mode)),d=z(ie(a.value)),b=j(()=>e(l)==="iterations"?"Additional iterations":"Additional physical time"),x=j(()=>e(l)==="iterations"?"1":"any"),A=j(()=>e(l)==="iterations"?"1":"0");async function m(){if(!Number.isFinite(e(s))||e(s)<=0){await We("MPI ranks must be an integer > 0.","Invalid value");return}if(!Number.isFinite(e(i))||e(i)<=0){await We("Threads must be an integer > 0.","Invalid value");return}if(e(o)&&(!Number.isFinite(e(o))||e(o)<=0)){await We("Max parallel must be empty or > 0.","Invalid value");return}if(!Number.isFinite(e(d))||e(d)<=0){await We("Value must be > 0.","Invalid value");return}if(e(l)==="iterations"&&!Number.isInteger(e(d))){await We("Iterations must be an integer.","Invalid value");return}const N={n:e(s),nt:e(i),maxParallel:e(o)||null,restartMode:e(l),restartValue:e(d)};go({n:e(s),nt:e(i),maxParallel:e(o)||null,mode:e(l),value:e(d)}),ot(N)}{const N=c=>{var w=ko(),P=K(w);$e(P,{variant:"secondary",onclick:()=>ot(null),children:(y,C)=>{ke();var _=Ve("Cancel");u(y,_)},$$slots:{default:!0}});var h=$(P,2);$e(h,{variant:"warning",onclick:m,children:(y,C)=>{ke();var _=Ve("Restart");u(y,_)},$$slots:{default:!0}}),u(c,w)};let L=j(()=>r.cases.length),g=j(()=>r.cases.length>1?"s":"");hr(t,{title:"Restart Cases",titleId:"restart-dialog-title",get subtitle(){return`${e(L)??""} case${e(g)??""} selected`},onConfirm:m,footer:N,children:(c,w)=>{var P=Ro(),h=K(P),y=k(h);Ne(y,{text:"MPI Ranks (n)",children:(q,I)=>{var O=Co();Xe(O),et(O,()=>e(s),V=>f(s,V)),u(q,O)}});var C=$(y,2);Ne(C,{text:"OMP Threads (nt)",children:(q,I)=>{var O=So();Xe(O),et(O,()=>e(i),V=>f(i,V)),u(q,O)}});var _=$(C,2);Ne(_,{text:"Max Parallel",children:(q,I)=>{var O=$o();Xe(O),et(O,()=>e(o),V=>f(o,V)),u(q,O)}}),p(h);var M=$(h,2),F=k(M);Ne(F,{text:"Stop criterion",children:(q,I)=>{var O=Po(),V=k(O);V.value=V.__value="iterations";var G=$(V);G.value=G.__value="physical_time",p(O),Hs(O,()=>e(l),Z=>f(l,Z)),u(q,O)}});var T=$(F,2);Ne(T,{get text(){return e(b)},children:(q,I)=>{var O=Eo();Xe(O),J(()=>{we(O,"min",e(A)),we(O,"step",e(x))}),et(O,()=>e(d),V=>f(d,V)),u(q,O)}}),p(M),u(c,P)},$$slots:{footer:!0,default:!0}})}Ae()}function Te(t,r){let a=me(r,"size",3,14);var n=be(),s=K(n);ks(s,()=>r.icon,(i,o)=>{o(i,{get size(){return a()},class:"icon"})}),u(t,n)}/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const Mo={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const Ao=t=>{for(const r in t)if(r.startsWith("aria-")||r==="role"||r==="title")return!0;return!1};/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const zn=(...t)=>t.filter((r,a,n)=>!!r&&r.trim()!==""&&n.indexOf(r)===a).join(" ").trim();var Lo=Ot("");function Be(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]),n=Oe(a,["name","color","size","strokeWidth","absoluteStrokeWidth","iconNode"]);Me(r,!1);let s=me(r,"name",8,void 0),i=me(r,"color",8,"currentColor"),o=me(r,"size",8,24),l=me(r,"strokeWidth",8,2),d=me(r,"absoluteStrokeWidth",8,!1),b=me(r,"iconNode",24,()=>[]);Rs();var x=Lo();Tn(x,(N,L,g)=>({...Mo,...N,...n,width:o(),height:o(),stroke:i(),"stroke-width":L,class:g}),[()=>Ao(n)?void 0:{"aria-hidden":"true"},()=>(Vt(d()),Vt(l()),Vt(o()),Mr(()=>d()?Number(l())*24/Number(o()):l())),()=>(Vt(zn),Vt(s()),Vt(a),Mr(()=>zn("lucide-icon","lucide",s()?`lucide-${s()}`:"",a.class)))]);var A=k(x);Ye(A,1,b,fn,(N,L)=>{var g=j(()=>ys(e(L),2));let c=()=>e(g)[0],w=()=>e(g)[1];var P=be(),h=K(P);Ts(h,c,!0,(y,C)=>{Tn(y,()=>({...w()}))}),u(N,P)});var m=$(A);He(m,r,"default",{}),p(x),u(t,x),Ae()}function To(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M12 5v14"}],["path",{d:"m19 12-7 7-7-7"}]];Be(t,Ue({name:"arrow-down"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Fo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M8 3 4 7l4 4"}],["path",{d:"M4 7h16"}],["path",{d:"m16 21 4-4-4-4"}],["path",{d:"M20 17H4"}]];Be(t,Ue({name:"arrow-left-right"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Do(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"m5 12 7-7 7 7"}],["path",{d:"M12 19V5"}]];Be(t,Ue({name:"arrow-up"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function pn(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"m6 9 6 6 6-6"}]];Be(t,Ue({name:"chevron-down"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function zo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"m9 18 6-6-6-6"}]];Be(t,Ue({name:"chevron-right"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Io(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["circle",{cx:"12",cy:"12",r:"10"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]];Be(t,Ue({name:"circle-stop"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Oo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]];Be(t,Ue({name:"circle-x"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function pr(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]];Be(t,Ue({name:"download"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Ho(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"}],["path",{d:"M12.56 6.6A10.97 10.97 0 0 0 14 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 0 1-11.91 4.97"}]];Be(t,Ue({name:"droplets"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function jo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}]];Be(t,Ue({name:"ellipsis"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Vo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M15 3h6v6"}],["path",{d:"M10 14 21 3"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}]];Be(t,Ue({name:"external-link"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Uo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"}],["path",{d:"M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"}]];Be(t,Ue({name:"fast-forward"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Bo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1"}]];Be(t,Ue({name:"pause"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function qo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M13 21h8"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]];Be(t,Ue({name:"pen-line"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Ko(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]];Be(t,Ue({name:"pencil"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function _a(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"}]];Be(t,Ue({name:"play"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Go(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]];Be(t,Ue({name:"plus"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function er(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]];Be(t,Ue({name:"refresh-cw"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Wo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}]];Be(t,Ue({name:"rotate-ccw"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Xo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7"}]];Be(t,Ue({name:"save"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Yo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"}],["circle",{cx:"12",cy:"12",r:"3"}]];Be(t,Ue({name:"settings"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}function Qo(t,r){const a=Oe(r,["children","$$slots","$$events","$$legacy"]);/** - * @license lucide-svelte v1.0.1 - ISC - * - * ISC License - * - * Copyright (c) 2026 Lucide Icons and Contributors - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * --- - * - * The following Lucide icons are derived from the Feather project: - * - * airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out - * - * The MIT License (MIT) (for the icons listed above) - * - * Copyright (c) 2013-present Cole Bemis - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */const n=[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]];Be(t,Ue({name:"trash-2"},()=>a,{get iconNode(){return n},children:(s,i)=>{var o=be(),l=K(o);He(l,r,"default",{}),u(s,o)},$$slots:{default:!0}}))}var Zo=S('
    • '),Jo=S('
        '),ei=S('
        ',1);function lt(t,r){Me(r,!0);let a=me(r,"value",3,""),n=me(r,"placeholder",3,"Select..."),s=me(r,"class",3,""),i=me(r,"buttonClass",3,""),o=z(!1),l=z(void 0),d=z(void 0),b=z(""),x=j(()=>{var T;return((T=r.options.find(q=>q.value===a()))==null?void 0:T.label)??n()});function A(T){var q;f(o,!1),(q=r.onchange)==null||q.call(r,T)}function m(){if(!e(l)||!e(d))return;const T=e(l).getBoundingClientRect(),q=e(d).offsetHeight,I=4,O=window.innerHeight-T.bottom-I,G=OO?T.top-I-q:T.bottom+I,Z=Math.min(T.left,window.innerWidth-T.width);f(b,`position:fixed; top:${G}px; left:${Z}px; width:${T.width}px; z-index:9999;`)}async function N(){f(o,!e(o)),e(o)&&(await Pt(),m())}function L(T){T.key==="Escape"&&f(o,!1)}function g(T){e(o)&&e(l)&&!e(l).contains(T.target)&&e(d)&&!e(d).contains(T.target)&&f(o,!1)}var c=ei();tt("mousedown",it,g),tt("keydown",it,L),tt("scroll",it,()=>{e(o)&&f(o,!1)});var w=K(c),P=k(w),h=k(P),y=k(h,!0);p(h);var C=$(h,2),_=k(C);Te(_,{get icon(){return pn},size:14}),p(C),p(P),nt(P,T=>f(l,T),()=>e(l)),p(w);var M=$(w,2);{var F=T=>{Jt(T,{children:(q,I)=>{var O=Jo();Ye(O,21,()=>r.options,V=>V.value,(V,G)=>{var Z=Zo();we(Z,"tabindex",0);var ae=k(Z,!0);p(Z),J(()=>{we(Z,"aria-selected",e(G).value===a()),Se(Z,1,`px-2.5 py-1.5 text-[13px] cursor-pointer transition-colors duration-100 - ${e(G).value===a()?"text-edf-bleu-fonce font-bold bg-[rgba(16,87,200,0.06)]":"text-ink hover:bg-edf-gris-clair"}`),ne(ae,e(G).label)}),le("mousedown",Z,E=>{E.stopPropagation(),A(e(G).value)}),u(V,Z)}),p(O),nt(O,V=>f(d,V),()=>e(d)),J(()=>Ze(O,e(b))),u(q,O)}})};Q(M,T=>{e(o)&&T(F)})}J(()=>{Se(w,1,`relative inline-flex ${s()??""}`),Se(P,1,dt(i()||"flex items-center justify-between gap-1.5 w-full h-[30px] cursor-pointer bg-white border border-border rounded-md text-[13px] text-ink font-normal pl-2.5 pr-2 transition-colors duration-150 hover:bg-edf-gris-clair focus:outline-none focus:border-edf-bleu-moyen")),we(P,"aria-expanded",e(o)),ne(y,e(x)),Se(C,1,`text-muted shrink-0 transition-transform duration-150 ${e(o)?"rotate-180":""}`)}),le("mousedown",P,T=>{T.stopPropagation(),N()}),u(t,c),Ae()}Ke(["mousedown"]);var ti=Ot(''),ri=S('
        '),ni=S('
        '),ai=S('
        ',1);function Gt(t,r){Me(r,!0);let a=me(r,"selected",19,()=>[]),n=me(r,"placeholder",3,"Select..."),s=me(r,"class",3,""),i=z(!1),o=z(void 0),l=z(void 0),d=z(""),b=j(()=>new Set(a())),x=j(()=>a().length===0?n():a().length===r.options.length?`All (${r.options.length})`:a().length<=2?a().map(I=>{var O;return((O=r.options.find(V=>V.value===I))==null?void 0:O.label)??I}).join(", "):`${a().length} selected`);function A(I){var V;const O=new Set(e(b));O.has(I)?O.delete(I):O.add(I),(V=r.onchange)==null||V.call(r,[...O])}function m(){var I;(I=r.onchange)==null||I.call(r,r.options.map(O=>O.value))}function N(){var I;(I=r.onchange)==null||I.call(r,[])}function L(){if(!e(o)||!e(l))return;const I=e(o).getBoundingClientRect(),O=e(l).offsetHeight,V=4,G=window.innerHeight-I.bottom-V,ae=GG?I.top-V-O:I.bottom+V,E=Math.min(I.left,window.innerWidth-I.width);f(d,`position:fixed; top:${ae}px; left:${E}px; min-width:${I.width}px; z-index:9999;`)}async function g(){f(i,!e(i)),e(i)&&(await Pt(),L())}function c(I){e(i)&&e(o)&&!e(o).contains(I.target)&&e(l)&&!e(l).contains(I.target)&&f(i,!1)}function w(I){I.key==="Escape"&&f(i,!1)}var P=ai();tt("mousedown",it,c),tt("keydown",it,w),tt("scroll",it,()=>{e(i)&&f(i,!1)});var h=K(P),y=k(h),C=k(y),_=k(C,!0);p(C);var M=$(C,2),F=k(M);Te(F,{get icon(){return pn},size:14}),p(M),p(y),nt(y,I=>f(o,I),()=>e(o)),p(h);var T=$(h,2);{var q=I=>{Jt(I,{children:(O,V)=>{var G=ni(),Z=k(G),ae=k(Z),E=$(ae,2);p(Z);var R=$(Z,2);Ye(R,17,()=>r.options,H=>H.value,(H,D)=>{var v=ri(),B=k(v),X=k(B);{var ue=se=>{var de=ti();u(se,de)},fe=j(()=>e(b).has(e(D).value));Q(X,se=>{e(fe)&&se(ue)})}p(B);var W=$(B,2),te=k(W,!0);p(W),p(v),J(se=>{Se(B,1,`inline-flex items-center justify-center shrink-0 w-[14px] h-[14px] rounded border-2 transition-colors duration-100 - ${se??""}`),ne(te,e(D).label)},[()=>e(b).has(e(D).value)?"bg-edf-bleu-moyen border-edf-bleu-moyen text-white":"bg-white border-edf-gris-moyen"]),le("mousedown",v,se=>{se.stopPropagation(),A(e(D).value)}),u(H,v)}),p(G),nt(G,H=>f(l,H),()=>e(l)),J(()=>Ze(G,e(d))),le("mousedown",ae,H=>{H.stopPropagation(),m()}),le("mousedown",E,H=>{H.stopPropagation(),N()}),u(O,G)}})};Q(T,I=>{e(i)&&I(q)})}J(()=>{Se(h,1,`relative inline-flex ${s()??""}`),we(y,"aria-expanded",e(i)),ne(_,e(x)),Se(M,1,`text-muted shrink-0 transition-transform duration-150 ${e(i)?"rotate-180":""}`)}),le("mousedown",y,I=>{I.stopPropagation(),g()}),u(t,P),Ae()}Ke(["mousedown"]);const Zr="csauto_token";let ba=z(ie(localStorage.getItem(Zr)??""));function xa(){return e(ba)}function ya(t){f(ba,t,!0),t?localStorage.setItem(Zr,t):localStorage.removeItem(Zr)}let wr=null;async function mn(t,r={}){const a=new Headers(r.headers),n=xa();n&&a.set("X-CSAUTO-TOKEN",n);const s=await fetch(t,{...r,headers:a});if(s.status===401){wr||(wr=hn("API token required:",n,"Authentication"));const i=await wr;return wr=null,i===null?s:(ya(i),a.set("X-CSAUTO-TOKEN",i),fetch(t,{...r,headers:a}))}return s}async function ct(t){const r=await mn(t);if(!r.ok)throw new Error(`GET ${t} failed: ${r.status}`);return r.json()}async function Ir(t){const r=await mn(t);if(!r.ok)throw new Error(`GET ${t} failed: ${r.status}`);return r.text()}async function Rt(t,r){const a=await mn(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!a.ok){const n=await a.text().catch(()=>a.statusText);throw new Error(`POST ${t} failed: ${a.status} — ${n}`)}return a.json()}function Or(t){return t.map(r=>`case=${encodeURIComponent(r)}`).join("&")}function si(t=!1){return ct(`/api/status${t?"?log=1":""}`)}function In(t){return ct(`/api/perf?${Or(t)}`)}function oi(){return ct("/api/app_config")}function ii(t){return ct(`/api/residual_columns?${Or(t)}`).then(r=>r.columns??[])}function li(t,r,a={}){const n=new URLSearchParams;return t.forEach(s=>n.append("case",s)),n.set("columns",r.join(",")),a.width&&n.set("width",String(a.width)),a.height&&n.set("height",String(a.height)),a.xMin!==void 0&&n.set("x_min",String(a.xMin)),a.includeHistory&&n.set("include_history","true"),Ir(`/api/residuals_svg?${n}`)}function wa(t){return ct(`/api/restart_origin?${Or(t)}`)}function ci(t){var a;const r=new URLSearchParams;return t.cases.forEach(n=>r.append("case",n)),(a=t.files)!=null&&a.length&&r.set("files",t.files.join(",")),t.maxHits&&r.set("max_hits",String(t.maxHits)),t.context!==void 0&&r.set("context",String(t.context)),t.sev&&r.set("sev",t.sev),t.q&&r.set("q",t.q),ct(`/api/recent_errors?${r}`)}function ui(t,r,a){const n=new URLSearchParams({case:t,file:r,n:String(a)});return Ir(`/api/tail?${n}`)}function di(t){return ct(`/api/resu_files?case=${encodeURIComponent(t)}`).then(r=>r.files??[])}function fi(t){return ct(`/api/resu_dirs?${Or(t)}`).then(r=>r.dirs??[])}function Jr(t,r){const a=new URLSearchParams({scope:r});return t.forEach(n=>a.append("case",n)),ct(`/api/probes?${a}`).then(n=>n.files??[])}function vi(t,r){const a=new URLSearchParams;return t.forEach(n=>a.append("case",n)),r.forEach(n=>a.append("probe",n)),ct(`/api/probe_columns?${a}`).then(n=>n.columns??[])}function gi(t,r,a){const n=new URLSearchParams({case:t,probe:r});return a.forEach(s=>n.append("column",s)),ct(`/api/probe_position?${n}`)}function hi(t){const r=new URLSearchParams;return t.cases.forEach(a=>r.append("case",a)),t.probes.forEach(a=>r.append("probe",a)),r.set("columns",t.columns.join(",")),t.axis&&r.set("axis",t.axis),t.xMin!==void 0&&r.set("x_min",String(t.xMin)),t.timeMin!==void 0&&r.set("time_min",String(t.timeMin)),r.set("include_history","true"),t.width&&r.set("width",String(t.width)),t.height&&r.set("height",String(t.height)),Ir(`/api/probe_svg?${r}`)}function pi(t){const r=new URLSearchParams;return t.cases.forEach(a=>r.append("case",a)),t.base&&r.set("base",t.base),t.kind&&r.set("kind",t.kind),t.filter&&r.set("filter",t.filter),Ir(`/api/compare_runs?${r}`)}function On(t){return Rt("/api/run_case",{cases:t.cases,n:t.n,nt:t.nt,max_parallel:t.maxParallel??void 0,restart:t.restart??!1,restart_mode:t.restartMode??"",restart_value:t.restartValue??void 0}).then(()=>{})}function mi(t){return Rt("/api/kill_case",{cases:t}).then(()=>{})}function kr(t){return Rt("/api/control_case",{cases:t.cases,action:t.action,value:t.value??void 0}).then(()=>{})}function _i(t){return Rt("/api/cleanup_cases",{cases:t.cases,keep_last:t.keepLast??1,prune_resu:t.pruneResu??!0,keep_resu:t.keepResu??[],delete_resu:t.deleteResu??[],max_log_mb:t.maxLogMb??50,clear_cid:!0,clear_pyc:!1})}function bi(t,r){return Rt("/api/case_note",{case:t,note:r}).then(()=>{})}function xi(t,r){return Rt("/api/case_convergence",{case:t,convergence:r}).then(()=>{})}function yi(t){return Rt("/api/open_gui",{case:t}).then(()=>{})}function wi(){return ct("/api/settings/telemetry")}function ki(t){return Rt("/api/settings/telemetry",{enabled:t})}var Ci=S(" ",1),Si=S(''),$i=S('Loading...'),Pi=S('No folders found'),Ei=S('
        '),Ri=S('
        ',1);function Ni(t,r){Me(r,!0);const a=ho();let n=z("keep_latest"),s=z(ie(a.keepLast)),i=z(ie([])),o=z(ie([])),l=z(!0),d=j(()=>e(n)==="keep_folder"||e(n)==="delete_folder");const b=[{value:"keep_latest",label:"Keep latest N"},{value:"delete_all",label:"Delete all RESU"},{value:"keep_folder",label:"Keep specific folders"},{value:"delete_folder",label:"Delete specific folders"}];let x=j(()=>e(i).map(m=>({value:m,label:m})));Zn(async()=>{try{f(i,await fi(r.cases),!0)}catch{f(i,[],!0)}f(l,!1)});async function A(){if(e(d)&&e(o).length===0){await We("Please select at least one RESU folder.","Missing selection");return}const m={action:e(n)};e(n)==="keep_latest"?(m.keepLast=e(s),po({keepLast:e(s)})):e(n)==="delete_all"?m.keepLast=0:e(n)==="keep_folder"?m.keepResu=e(o):e(n)==="delete_folder"&&(m.deleteResu=e(o)),ot(m)}{const m=g=>{var c=Ci(),w=K(c);$e(w,{variant:"secondary",onclick:()=>ot(null),children:(h,y)=>{ke();var C=Ve("Cancel");u(h,C)},$$slots:{default:!0}});var P=$(w,2);$e(P,{variant:"primary",onclick:A,children:(h,y)=>{ke();var C=Ve("Clean");u(h,C)},$$slots:{default:!0}}),u(g,c)};let N=j(()=>r.cases.length),L=j(()=>r.cases.length>1?"s":"");hr(t,{title:"Cleanup Cases",titleId:"clean-dialog-title",get subtitle(){return`${e(N)??""} case${e(L)??""} selected`},footer:m,children:(g,c)=>{var w=Ri(),P=K(w),h=k(P);Ne(h,{text:"Action",children:(F,T)=>{lt(F,{class:"w-[200px]",get options(){return b},get value(){return e(n)},onchange:q=>f(n,q,!0)})}});var y=$(h,2);{var C=F=>{Ne(F,{text:"Keep last",children:(T,q)=>{var I=Si();Xe(I),et(I,()=>e(s),O=>f(s,O)),u(T,I)}})};Q(y,F=>{e(n)==="keep_latest"&&F(C)})}p(P);var _=$(P,2);{var M=F=>{var T=Ei(),q=k(T);Ne(q,{text:"RESU folders",children:(I,O)=>{var V=be(),G=K(V);{var Z=R=>{var H=$i();u(R,H)},ae=R=>{var H=Pi();u(R,H)},E=R=>{Gt(R,{class:"w-full",get options(){return e(x)},get selected(){return e(o)},onchange:H=>f(o,H,!0),placeholder:"Select folders..."})};Q(G,R=>{e(l)?R(Z):e(i).length===0?R(ae,1):R(E,-1)})}u(I,V)}}),p(T),u(F,T)};Q(_,F=>{e(d)&&F(M)})}u(g,w)},$$slots:{footer:!0,default:!0}})}Ae()}function Mi(t,r){Me(r,!0);let a=j(Ws);var n=be(),s=K(n);{var i=b=>{wo(b,{get cases(){return e(a).props.cases}})},o=b=>{No(b,{get cases(){return e(a).props.cases}})},l=b=>{Ni(b,{get cases(){return e(a).props.cases}})},d=b=>{io(b,{get mode(){return e(a).type},get title(){return e(a).props.title},get message(){return e(a).props.message},get confirmLabel(){return e(a).props.confirmLabel},get confirmVariant(){return e(a).props.confirmVariant},get value(){return e(a).props.value},get placeholder(){return e(a).props.placeholder},get multiline(){return e(a).props.multiline}})};Q(s,b=>{e(a).type==="run"?b(i):e(a).type==="restart"?b(o,1):e(a).type==="clean"?b(l,2):(e(a).type==="alert"||e(a).type==="confirm"||e(a).type==="prompt")&&b(d,3)})}u(t,n),Ae()}let fr=z(ie([])),Ai=0;function Li(){return e(fr)}function Cr(t,r="success",a=3500){const n=++Ai;f(fr,[...e(fr),{id:n,message:t,variant:r}],!0),setTimeout(()=>ka(n),a)}function ka(t){f(fr,e(fr).filter(r=>r.id!==t),!0)}var Ti=S('
        '),Fi=S('
        ');function Di(t,r){Me(r,!0);let a=j(Li);Jt(t,{children:(n,s)=>{var i=Fi();Ye(i,21,()=>e(a),o=>o.id,(o,l)=>{var d=Ti(),b=k(d,!0);p(d),J(()=>{Se(d,1,`pointer-events-auto max-w-[320px] px-4 py-2.5 rounded-md shadow-lg text-sm font-semibold text-white cursor-pointer ${e(l).variant==="success"?"bg-edf-vert-fonce":"bg-edf-orange-fonce"}`),ne(b,e(l).message)}),le("click",d,()=>ka(e(l).id)),u(o,d)}),p(i),u(n,i)}}),Ae()}Ke(["click"]);var zi=Ot(''),Ii=Ot(''),Oi=S(''),Hi=S(" ",1),ji=S(" ",1),Vi=S('');function It(t,r){Me(r,!0);const a=g=>{var c=Oi(),w=k(c);{var P=y=>{var C=zi();J(()=>Ze(C,`width: ${o()-4}px; height: ${o()-4}px;`)),u(y,C)},h=y=>{var C=Ii();J(()=>Ze(C,`width: ${o()-4}px; height: ${o()-4}px;`)),u(y,C)};Q(w,y=>{n()&&!s()?y(P):s()&&y(h,1)})}p(c),J(()=>{Se(c,1,`inline-flex items-center justify-center rounded border-2 cursor-pointer transition-colors duration-100 shrink-0 - ${i()?"opacity-40 cursor-not-allowed":""} - ${n()||s()?"bg-edf-bleu-moyen border-edf-bleu-moyen text-white":"bg-white border-edf-gris-moyen hover:border-edf-bleu-clair"}`),Ze(c,`width: ${o()??""}px; height: ${o()??""}px;`),we(c,"aria-checked",s()?"mixed":n()),we(c,"aria-disabled",i()),we(c,"tabindex",i()?-1:0)}),le("click",c,b),le("keydown",c,x),u(g,c)};let n=me(r,"checked",3,!1),s=me(r,"indeterminate",3,!1),i=me(r,"disabled",3,!1),o=me(r,"size",3,16),l=me(r,"label",3,""),d=me(r,"labelFirst",3,!1);function b(g){var w;if(g.stopPropagation(),i())return;const c=s()?!0:!n();(w=r.onchange)==null||w.call(r,c)}function x(g){(g.key===" "||g.key==="Enter")&&(g.preventDefault(),g.stopPropagation(),b(g))}var A=be(),m=K(A);{var N=g=>{var c=Vi(),w=k(c);{var P=y=>{var C=Hi(),_=K(C),M=k(_,!0);p(_);var F=$(_);a(F),J(()=>ne(M,l())),u(y,C)},h=y=>{var C=ji(),_=K(C);a(_);var M=$(_),F=k(M,!0);p(M),J(()=>ne(F,l())),u(y,C)};Q(w,y=>{d()?y(P):y(h,-1)})}p(c),le("click",c,b),le("keydown",c,x),u(g,c)},L=g=>{a(g)};Q(m,g=>{l()?g(N):g(L,-1)})}u(t,A),Ae()}Ke(["click","keydown"]);function _n(t,r){try{const a=localStorage.getItem(t);return a===null?r:JSON.parse(a)}catch{return r}}function Hr(t,r){localStorage.setItem(t,JSON.stringify(r))}const en=new Map;function mr(t,r,a){vt(t),en.set(t,setInterval(r,a))}function vt(t){const r=en.get(t);r!==void 0&&(clearInterval(r),en.delete(t))}const Tt={status:1e3,tail:1e3,plot:3e3,probe:3e3,errors:5e3},Ui=["status","plot","probe","tail","errors"],Wr={status:{label:"Status table",min:500},plot:{label:"Residual plots",min:1e3},probe:{label:"Probe plots",min:1e3},tail:{label:"Log tail",min:500},errors:{label:"Recent errors",min:2e3}},ir=_n("csauto_refresh_rates",{});let bt=z(ie({status:ir.status??Tt.status,tail:ir.tail??Tt.tail,plot:ir.plot??Tt.plot,probe:ir.probe??Tt.probe,errors:ir.errors??Tt.errors}));function Bi(){return e(bt)}function qi(t,r){f(bt,{...e(bt),[t]:r},!0),Hr("csauto_refresh_rates",e(bt))}function Ki(){return e(bt).status}function Hn(){return e(bt).tail}function jn(){return e(bt).plot}function Gi(){return e(bt).probe}function Vn(){return e(bt).errors}const tn=new Set;function _r(t){return tn.add(t),()=>tn.delete(t)}function Wi(){tn.forEach(t=>t())}const rn=_n("csauto_autorefresh_enabled",{});function tr(t){return rn[t]??!0}function rr(t,r){rn[t]=r,Hr("csauto_autorefresh_enabled",rn)}var Xi=S(" ",1),Yi=S('
        ms
        '),Qi=S('
        Telemetry
        '),Zi=S('
        API token
        Auto-refresh intervals
        ',1);function Ji(t,r){Me(r,!0);let a=z(ie(xa())),n=z(ie({...Bi()})),s=z(!0),i=z(!1);Re(()=>{wi().then(b=>{f(s,b.enabled,!0),f(i,!0)}).catch(()=>{f(i,!1)})});function o(b){const{min:x}=Wr[b];let A=e(n)[b];(typeof A!="number"||isNaN(A))&&(A=Tt[b]),f(n,{...e(n),[b]:Math.max(x,Math.round(A))},!0)}function l(b,x){const A=x.target,m=A.value.replace(/[^0-9]/g,""),N=parseInt(m,10);isNaN(N)?m===""&&f(n,{...e(n),[b]:Wr[b].min},!0):f(n,{...e(n),[b]:N},!0),A.value=String(e(n)[b])}function d(){ya(e(a));for(const b of Object.keys(e(n)))o(b),qi(b,e(n)[b]);e(i)&&ki(e(s)).catch(()=>{}),r.onClose()}hr(t,{title:"Settings",titleId:"settings-dialog-title",get onCancel(){return r.onClose},onConfirm:d,maxWidth:"400px",portal:!0,footer:x=>{var A=Xi(),m=K(A);$e(m,{variant:"secondary",get onclick(){return r.onClose},children:(L,g)=>{ke();var c=Ve("Cancel");u(L,c)},$$slots:{default:!0}});var N=$(m,2);$e(N,{variant:"primary",onclick:d,children:(L,g)=>{ke();var c=Ve("Save");u(L,c)},$$slots:{default:!0}}),u(x,A)},children:(x,A)=>{var m=Zi(),N=K(m),L=$(k(N),2);Xe(L),p(N);var g=$(N,2),c=k(g),w=$(k(c),2);p(c);var P=$(c,2);Ye(P,20,()=>Ui,C=>C,(C,_)=>{const M=j(()=>Wr[_]);var F=Yi(),T=k(F),q=k(T),I=k(q,!0);p(q);var O=$(q,2),V=k(O);p(O),p(T);var G=$(T,2),Z=k(G);Xe(Z),ke(2),p(G),p(F),J(()=>{ne(I,e(M).label),ne(V,`min ${e(M).min??""}ms`),vn(Z,e(n)[_])}),le("input",Z,ae=>l(_,ae)),tt("blur",Z,()=>o(_)),u(C,F)}),p(P),p(g);var h=$(g,2);{var y=C=>{var _=Qi(),M=$(k(_),2);It(M,{get checked(){return e(s)},onchange:F=>f(s,F,!0),label:"Send anonymous usage statistics"}),p(_),u(C,_)};Q(h,C=>{e(i)&&C(y)})}et(L,()=>e(a),C=>f(a,C)),le("click",w,()=>f(n,{...Tt},!0)),u(x,m)},$$slots:{footer:!0,default:!0}}),Ae()}Ke(["click","input"]);const el=""+new URL("../assets/code-saturne.BHojVttu.svg",import.meta.url).href,tl="data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'?%3e%3csvg%20id='Calque_1'%20xmlns='http://www.w3.org/2000/svg'%20version='1.1'%20height='400px'%20viewBox='0%200%20397.89%2095.85'%3e%3c!--%20Generator:%20Adobe%20Illustrator%2029.8.1,%20SVG%20Export%20Plug-In%20.%20SVG%20Version:%202.1.1%20Build%202)%20--%3e%3cdefs%3e%3cstyle%3e%20.st0%20{%20fill:%20%23ff861d;%20}%20.st1%20{%20fill:%20%23ffb210;%20}%20.st2%20{%20fill:%20%231057c8;%20}%20.st3%20{%20fill:%20%23d6430a;%20}%20%3c/style%3e%3c/defs%3e%3cpath%20class='st2'%20d='M26.25,61.73c-12.02,0-20.11-8.16-20.11-21.37s8.01-21.37,20.33-21.37c8.83,0,15.06,3.78,17.36,11.28l-5.64,2.52c-1.48-5.94-4.97-8.69-11.87-8.69-8.46,0-14.25,6.01-14.25,16.25s5.93,16.25,13.95,16.25,11.73-2.82,12.84-8.61l5.71,1.93c-2,7.79-9.13,11.8-18.33,11.8'/%3e%3cpath%20class='st2'%20d='M89.96,40.36c0,13.21-7.87,21.37-20.48,21.37s-20.48-8.16-20.48-21.37,7.86-21.37,20.48-21.37,20.48,8.16,20.48,21.37M54.94,40.36c0,10.24,5.64,16.25,14.54,16.25s14.54-6.01,14.54-16.25-5.64-16.25-14.54-16.25-14.54,6.01-14.54,16.25'/%3e%3cpath%20class='st2'%20d='M134.3,40.29c0,13.43-8.01,20.85-21.81,20.85h-14.84V19.58h14.84c13.8,0,21.81,7.42,21.81,20.7M128.36,40.29c0-9.94-5.57-15.58-15.44-15.58h-9.35v31.32h9.35c9.87,0,15.44-5.71,15.44-15.73'/%3e%3cpolygon%20class='st2'%20points='174.11%2056.02%20174.11%2061.14%20141.98%2061.14%20141.98%2019.58%20173.14%2019.58%20173.14%2024.7%20147.91%2024.7%20147.91%2037.54%20167.65%2037.54%20167.65%2042.66%20147.91%2042.66%20147.91%2056.02%20174.11%2056.02'/%3e%3cpath%20class='st2'%20d='M213.81,22.13c4.16-1.92,9.75-3.11,14.3-3.11,12.55,0,17.74,5.19,17.74,17.34v5.27c0,4.16.08,7.28.16,10.31.08,3.12.24,5.99.48,9.19h-9.43c-.4-2.16-.4-4.88-.48-6.15h-.16c-2.48,4.56-7.83,7.11-12.71,7.11-7.28,0-14.39-4.4-14.39-12.23,0-6.15,2.96-9.75,7.03-11.75,4.08-2,9.35-2.4,13.83-2.4h5.91c0-6.63-2.96-8.87-9.27-8.87-4.56,0-9.11,1.76-12.71,4.48l-.32-9.19ZM226.36,54.26c3.27,0,5.83-1.44,7.51-3.68,1.76-2.32,2.24-5.27,2.24-8.47h-4.63c-4.8,0-11.91.8-11.91,7.11,0,3.52,2.96,5.04,6.79,5.04'/%3e%3cpath%20class='st2'%20d='M280.98,28.61c-3.28-1.12-5.67-1.76-9.59-1.76-2.88,0-6.31,1.04-6.31,4.56,0,6.56,18.62,2.4,18.62,17.27,0,9.59-8.55,13.43-17.27,13.43-4.08,0-8.23-.72-12.15-1.76l.64-8.79c3.36,1.68,6.87,2.72,10.55,2.72,2.72,0,7.03-1.04,7.03-5.04,0-8.07-18.62-2.56-18.62-17.42,0-8.87,7.75-12.79,16.14-12.79,5.03,0,8.31.8,11.67,1.52l-.72,8.07Z'/%3e%3cpath%20class='st2'%20d='M295.25,27.81h-7.91v-7.83h7.91v-8.23l10.71-3.44v11.67h9.51v7.83h-9.51v19.18c0,3.52.96,6.79,5.04,6.79,1.92,0,3.76-.4,4.88-1.12l.32,8.47c-2.24.64-4.72.96-7.91.96-8.39,0-13.03-5.2-13.03-13.35v-20.94Z'/%3e%3cpath%20class='st2'%20d='M355.22,58.82c-3.92,2.16-8.39,3.28-14.15,3.28-13.59,0-21.5-7.83-21.5-21.34,0-11.91,6.31-21.74,19.1-21.74,15.27,0,19.58,10.47,19.58,24.85h-28.45c.48,6.63,5.11,10.39,11.75,10.39,5.2,0,9.67-1.92,13.67-4.15v8.71ZM348.03,36.52c-.32-5.19-2.72-9.67-8.71-9.67s-9.03,4.16-9.51,9.67h18.22Z'/%3e%3cpath%20class='st2'%20d='M366.57,19.98h9.51v9.35h.16c.48-3.84,4.87-10.31,11.27-10.31,1.04,0,2.16,0,3.27.32v10.79c-.96-.56-2.88-.88-4.79-.88-8.71,0-8.71,10.87-8.71,16.78v15.11h-10.71V19.98Z'/%3e%3cpath%20class='st0'%20d='M229.73,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3cpath%20class='st0'%20d='M203.17,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3cpath%20class='st1'%20d='M296.14,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3cpath%20class='st1'%20d='M378.47,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3cpath%20class='st1'%20d='M269.57,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3cpath%20class='st3'%20d='M107.55,80.9c0,3.67-2.97,6.64-6.64,6.64s-6.64-2.97-6.64-6.64,2.97-6.64,6.64-6.64,6.64,2.97,6.64,6.64'/%3e%3crect%20class='st0'%20x='134.12'%20y='75.59'%20width='42.5'%20height='10.62'/%3e%3crect%20class='st1'%20x='309.42'%20y='75.59'%20width='42.5'%20height='10.62'/%3e%3c/svg%3e";let Wt=z(null);function rl(t){f(Wt,t,!0)}function Ft(){return e(Wt)}function cr(t){return e(Wt)===null||e(Wt).capabilities.includes(t)}function nl(t){return e(Wt)===null||e(Wt).control_actions.includes(t)}var al=S('CODE_SATURNE'),sl=S('CODE_ASTER'),ol=S(' '),il=S('
        ',1);function ll(t,r){Me(r,!0);let a=j(()=>r.shownCases!==r.totalCases),n=z(!1);var s=il(),i=K(s),o=k(i),l=k(o);{var d=D=>{var v=al();J(()=>we(v,"src",el)),u(D,v)},b=j(()=>{var D;return!Ft()||((D=Ft())==null?void 0:D.solver)==="code_saturne"}),x=D=>{var v=sl();J(()=>we(v,"src",tl)),u(D,v)},A=j(()=>{var D;return((D=Ft())==null?void 0:D.solver)==="code_aster"}),m=D=>{var v=ol(),B=k(v,!0);p(v),J(X=>ne(B,X),[()=>{var X;return(X=Ft())==null?void 0:X.solver}]),u(D,v)};Q(l,D=>{e(b)?D(d):e(A)?D(x,1):D(m,-1)})}p(o);var N=$(o,2),L=k(N),g=k(L),c=k(g);{var w=D=>{var v=Ve();J(()=>ne(v,`${r.shownCases??""}/`)),u(D,v)};Q(c,D=>{e(a)&&D(w)})}var P=$(c,1,!0);p(g);var h=$(g,2),y=k(h,!0);p(h),p(L);var C=$(L,2),_=k(C),M=k(_);{var F=D=>{var v=Ve();J(()=>ne(v,`${r.shownRunning??""}/`)),u(D,v)};Q(M,D=>{e(a)&&D(F)})}var T=$(M,1,!0);p(_),ke(2),p(C);var q=$(C,2),I=k(q),O=k(I);{var V=D=>{var v=Ve();J(()=>ne(v,`${r.shownConverged??""}/`)),u(D,v)};Q(O,D=>{e(a)&&D(V)})}var G=$(O,1,!0);p(I),ke(2),p(q),p(N);var Z=$(N,2),ae=k(Z),E=k(ae);Te(E,{get icon(){return Yo},size:18}),p(ae),p(Z),p(i);var R=$(i,2);{var H=D=>{Ji(D,{onClose:()=>f(n,!1)})};Q(R,D=>{e(n)&&D(H)})}J(()=>{ne(P,r.totalCases),ne(y,r.totalCases===1?"case":"cases"),ne(T,r.totalRunning),ne(G,r.totalConverged)}),le("click",ae,()=>f(n,!0)),u(t,s),Ae()}Ke(["click"]);const cl=""+new URL("../assets/simvia-logo.kLDq7Uoj.svg",import.meta.url).href;var ul=S('
        '),dl=S('

        '),fl=S("
        "),vl=S('
        '),gl=S('
        '),hl=S('
        ');function Ht(t,r){let a=me(r,"eyebrow",3,""),n=me(r,"title",3,""),s=me(r,"wide",3,!1);var i=hl(),o=$(k(i),2);{var l=b=>{var x=gl(),A=k(x);{var m=c=>{var w=be(),P=K(w);ft(P,()=>r.titleSlot),u(c,w)},N=c=>{var w=fl(),P=k(w);{var h=_=>{var M=ul(),F=k(M,!0);p(M),J(()=>ne(F,a())),u(_,M)};Q(P,_=>{a()&&_(h)})}var y=$(P,2);{var C=_=>{var M=dl(),F=k(M,!0);p(M),J(()=>ne(F,n())),u(_,M)};Q(y,_=>{n()&&_(C)})}p(w),u(c,w)};Q(A,c=>{r.titleSlot?c(m):c(N,-1)})}var L=$(A,2);{var g=c=>{var w=vl(),P=k(w);ft(P,()=>r.actions),p(w),u(c,w)};Q(L,c=>{r.actions&&c(g)})}p(x),u(b,x)};Q(o,b=>{(a()||n()||r.titleSlot||r.actions)&&b(l)})}var d=$(o,2);ft(d,()=>r.children),p(i),J(()=>{Se(i,1,`${s()?"col-span-12":"col-span-6"} bg-card border border-border rounded-[10px] p-[16px_18px_18px] relative overflow-clip animate-rise`),we(i,"id",r.id)}),u(t,i)}function nr(t,r){Me(r,!0);let a=me(r,"checked",15,!0);Re(()=>{a()?mr(r.name,r.onRefresh,r.intervalMs):vt(r.name)}),vr(()=>vt(r.name)),It(t,{get checked(){return a()},onchange:n=>a(n),size:14,label:"Auto-refresh",labelFirst:!0}),Ae()}var pl=S(" ",1),ml=S('
      • '),_l=S(''),bl=S(' ',1);function xl(t,r){Me(r,!0);let a=me(r,"label",3,"More"),n=z(!1),s=z(void 0),i=z(void 0),o=z("");function l(){if(!e(s)||!e(i))return;const c=e(s).getBoundingClientRect(),w=e(i).offsetHeight,P=4,h=window.innerHeight-c.bottom-P,C=hh?c.top-P-w:c.bottom+P,_=Math.min(c.left,window.innerWidth-180);f(o,`position:fixed; top:${C}px; left:${_}px; min-width:170px; z-index:9999;`)}async function d(){f(n,!e(n)),e(n)&&(await Pt(),l())}function b(c){c.disabled||(f(n,!1),c.onClick())}function x(c){e(n)&&e(s)&&!e(s).contains(c.target)&&e(i)&&!e(i).contains(c.target)&&f(n,!1)}var A=bl();tt("mousedown",it,x),tt("keydown",it,c=>{c.key==="Escape"&&f(n,!1)}),tt("scroll",it,()=>{e(n)&&f(n,!1)});var m=K(A),N=k(m);$e(N,{variant:"secondary",size:"sm",onclick:d,children:(c,w)=>{var P=pl(),h=K(P);Te(h,{get icon(){return jo}});var y=$(h);J(()=>ne(y,` ${a()??""}`)),u(c,P)},$$slots:{default:!0}}),p(m),nt(m,c=>f(s,c),()=>e(s));var L=$(m,2);{var g=c=>{Jt(c,{children:(w,P)=>{var h=_l();Ye(h,21,()=>r.items,y=>y.label,(y,C)=>{var _=ml(),M=k(_),F=k(M);Te(F,{get icon(){return e(C).icon},size:14});var T=$(F);p(M),p(_),J(()=>{M.disabled=e(C).disabled,we(M,"aria-disabled",e(C).disabled),Se(M,1,`w-full flex items-center gap-2 text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] text-ink transition-[background] duration-[120ms] ease-in-out disabled:opacity-40 disabled:pointer-events-none ${e(C).disabled?"":"cursor-pointer hover:bg-edf-gris-clair hover:border-border"}`),ne(T,` ${e(C).label??""}`)}),le("mousedown",M,q=>{q.stopPropagation(),b(e(C))}),u(y,_)}),p(h),nt(h,y=>f(i,y),()=>e(i)),J(()=>Ze(h,e(o))),u(w,h)}})};Q(L,c=>{e(n)&&c(g)})}u(t,A),Ae()}Ke(["mousedown"]);const Dt=[{key:"note",label:"Note"},{key:"nprocs",label:"MPI Ranks"},{key:"nt",label:"Thread Count"},{key:"last_iter",label:"Last Iter"},{key:"duration",label:"Duration"},{key:"last_mod",label:"Last Modified"},{key:"resu_size_mb",label:"RESU Size (MB)"}];let Xt=z(ie([])),Lr=z(ie([])),pt=z(ie(new Set)),br=z(""),bn=z(""),Et=z(ie([])),st=z(ie([])),Yt=z(""),Ca=z(!0),yl=z(!0),qt=z(ie(_n("csauto_status_views",{}))),xn=z(ie(localStorage.getItem("csauto_status_view_selected")??""));function yn(){return e(Xt)}function wn(){return e(Lr)}function Ut(){return e(pt)}function Un(){return e(br)}function nn(){return e(bn)}function wl(){return e(Et)}function kl(){return e(Et).filter(t=>e(Lr).includes(t))}function Cl(){return e(Et).filter(t=>Dt.some(r=>r.key===t))}function Sl(){return e(st)}function $l(){return e(Yt)}function Pl(){return e(Ca)}function Bn(){return e(qt)}function El(){return e(xn)}function Rl(t){f(Xt,t,!0)}function Nl(t){const r=e(Lr).length===0&&t.length>0;f(Lr,t,!0),r&&e(Et).length===0&&f(Et,[...t,...Dt.map(a=>a.key)],!0)}function Ml(t){f(br,t,!0)}function qn(t){f(Et,t,!0)}function Al(t){f(Yt,t,!0)}function Ll(t){f(Ca,t,!0)}function Tl(t){f(yl,t,!0)}function kn(t){f(xn,t,!0),localStorage.setItem("csauto_status_view_selected",t)}function an(t){const r=new Set(e(pt));r.has(t)?r.delete(t):r.add(t),f(pt,r,!0),f(br,t,!0),f(bn,t,!0)}function Sa(t){f(pt,new Set([t]),!0),f(br,t,!0),f(bn,t,!0)}function sn(t,r,a){const n=a.indexOf(t),s=a.indexOf(r);if(n<0||s<0)return;const[i,o]=n{const a=Fl(r);return t.every(n=>a.includes(n))})}function Dl(t){if(t==null||t==="")return"";const r=Number(t);return Number.isFinite(r)?r:String(t).toLowerCase()}function Wn(t,r){var n;if(r==="case_id")return t.case_id;if(r==="status")return t.status??"";if(r==="note")return t.note??"";if(r==="nprocs")return t.nprocs??0;if(r==="nt")return t.nt??0;if(r==="last_iter")return t.last_iter??0;if(r==="duration")return t.duration_s??0;if(r==="last_mod")return t.last_mod??"";if(r==="resu_size_mb")return t.resu_size_mb??0;const a=(n=t.doe)==null?void 0:n[r];return a!==void 0?Dl(a):""}function zl(t,r){return t===""&&r===""?0:t===""?1:r===""?-1:typeof t=="number"&&typeof r=="number"?t-r:String(t).localeCompare(String(r))}function Pa(){const t=$a();return e(st).length===0?t:[...t].sort((r,a)=>{for(const n of e(st)){const s=Wn(r,n.key),i=Wn(a,n.key),o=zl(s,i);if(o!==0)return n.dir==="asc"?o:-o}return 0})}function Rr(){return Pa().map(t=>t.case_id)}function Il(t,r){const a=e(st).findIndex(n=>n.key===t);if(r)if(a>=0){const n=[...e(st)];n[a]={key:t,dir:n[a].dir==="asc"?"desc":"asc"},f(st,n,!0)}else f(st,[...e(st),{key:t,dir:"asc"}],!0);else a>=0&&e(st).length===1?f(st,[{key:t,dir:e(st)[0].dir==="asc"?"desc":"asc"}],!0):f(st,[{key:t,dir:"asc"}],!0)}function Ol(){return{visibleColumns:[...e(Et)],search:e(Yt),sorts:e(st).map(t=>({...t}))}}function Hl(t){f(Et,[...t.visibleColumns],!0),f(Yt,t.search,!0),f(st,t.sorts.map(r=>({...r})),!0)}function jl(t){f(qt,{...e(qt),[t]:Ol()},!0),Hr("csauto_status_views",e(qt)),kn(t)}function Vl(t){const r={...e(qt)};delete r[t],f(qt,r,!0),Hr("csauto_status_views",r),e(xn)===t&&kn("")}function Sr(){const t=new Set;for(const r of e(Xt))e(pt).has(r.case_id)&&t.add((r.status??"").toUpperCase());return t}function Xn(){for(const t of e(Xt))if(e(pt).has(t.case_id)&&t.resu_size_mb!=null&&t.resu_size_mb>0)return!0;return!1}function Ul(t,r){const a=r.length,n=t.length,s=r.filter(d=>d.status==="RUNNING").length,i=t.filter(d=>d.status==="RUNNING").length,o=r.filter(d=>d.convergence==="converged").length,l=t.filter(d=>d.convergence==="converged").length;return{totalCases:a,shownCases:n,totalRunning:s,shownRunning:i,totalConverged:o,shownConverged:l}}var Bl=S('
        DOE columns
        '),ql=S('
        ');function Kl(t,r){Me(r,!0);let a=me(r,"initialName",3,""),n=j(wn),s=z(ie(a())),i=z(ie(new Set(r.initialColumns??[...e(n),...Dt.map(V=>V.key)])));function o(V){const G=new Set(e(i));G.has(V)?G.delete(V):G.add(V),f(i,G,!0)}function l(V,G){const Z=new Set(e(i));V.forEach(ae=>G?Z.add(ae):Z.delete(ae)),f(i,Z,!0)}function d(){const V=e(s).trim();V&&r.onSave(V,[...e(i)])}function b(V){V.key==="Escape"&&r.onCancel()}function x(V){V.target.dataset.backdrop!==void 0&&r.onCancel()}let A=z(void 0);Re(()=>{var V,G;(V=e(A))==null||V.focus(),(G=e(A))==null||G.select()});var m=ql(),N=k(m),L=k(N),g=k(L,!0);p(L);var c=$(L,2),w=$(k(c),2);Xe(w),nt(w,V=>f(A,V),()=>e(A)),p(c);var P=$(c,2);{var h=V=>{var G=Bl(),Z=k(G),ae=$(k(Z),2),E=k(ae),R=$(E,2);p(ae),p(Z);var H=$(Z,2);Ye(H,20,()=>e(n),D=>D,(D,v)=>{{let B=j(()=>e(i).has(v));It(D,{get checked(){return e(B)},onchange:()=>o(v),size:14,get label(){return v}})}}),p(H),p(G),le("click",E,()=>l(e(n),!0)),le("click",R,()=>l(e(n),!1)),u(V,G)};Q(P,V=>{e(n).length>0&&V(h)})}var y=$(P,2),C=k(y),_=$(k(C),2),M=k(_),F=$(M,2);p(_),p(C);var T=$(C,2);Ye(T,21,()=>Dt,V=>V.key,(V,G)=>{{let Z=j(()=>e(i).has(e(G).key));It(V,{get checked(){return e(Z)},onchange:()=>o(e(G).key),size:14,get label(){return e(G).label}})}}),p(T),p(y);var q=$(y,2),I=k(q);$e(I,{variant:"secondary",get onclick(){return r.onCancel},children:(V,G)=>{ke();var Z=Ve("Cancel");u(V,Z)},$$slots:{default:!0}});var O=$(I,2);{let V=j(()=>!e(s).trim());$e(O,{variant:"primary",onclick:d,get disabled(){return e(V)},children:(G,Z)=>{ke();var ae=Ve("Save");u(G,ae)},$$slots:{default:!0}})}p(q),p(N),p(m),J(()=>ne(g,r.mode==="create"?"New view":"Edit view")),le("keydown",m,b),le("click",m,x),et(w,()=>e(s),V=>f(s,V)),le("click",M,()=>l(Dt.map(V=>V.key),!0)),le("click",F,()=>l(Dt.map(V=>V.key),!1)),u(t,m),Ae()}Ke(["keydown","click"]);var Gl=S('
        ',1);function Wl(t,r){Me(r,!0);let a=j(()=>Object.keys(Bn())),n=z(ie(El())),s=z(!1),i=z("create"),o=j(()=>[{value:"",label:"All columns"},...e(a).map(C=>({value:C,label:C}))]);function l(C){if(f(n,C,!0),kn(C),C==="")qn([...wn(),...Dt.map(_=>_.key)]);else{const _=Bn();_[C]&&Hl(_[C])}}function d(){f(i,"edit"),f(s,!0)}function b(){f(i,"create"),f(s,!0)}function x(C,_){qn(_),e(i)==="edit"&&e(n)&&C!==e(n)&&Vl(e(n)),jl(C),f(n,C,!0),f(s,!1)}function A(){f(s,!1)}var m=Gl(),N=K(m),L=k(N);lt(L,{class:"w-[140px]",get options(){return e(o)},get value(){return e(n)},onchange:l});var g=$(L,2),c=k(g);Te(c,{get icon(){return Ko},size:14}),p(g);var w=$(g,2),P=k(w);Te(P,{get icon(){return Go},size:14}),p(w),p(N);var h=$(N,2);{var y=C=>{Jt(C,{children:(_,M)=>{{let F=j(()=>e(i)==="edit"?e(n):""),T=j(wl);Kl(_,{get mode(){return e(i)},get initialName(){return e(F)},get initialColumns(){return e(T)},onSave:x,onCancel:A})}}})};Q(h,C=>{e(s)&&C(y)})}le("click",g,d),le("click",w,b),u(t,m),Ae()}Ke(["click"]);var Xl=S('
        '),Yl=S(""),Ql=S(' '),Zl=S(' '),Jl=S('
        '),ec=S(" "),tc=S(''),rc=S(''),nc=S(" Open GUI",1),ac=S(""),sc=S(" "),oc=S(""),ic=S('
        ');function lc(t,r){Me(r,!0);let a=j(()=>r.rows.length>0&&r.rows.every(E=>e(b).has(E.case_id))),n=j(()=>r.rows.some(E=>e(b).has(E.case_id))),s=j(()=>e(n)&&!e(a)),i=j(kl),o=j(()=>new Set(Cl())),l=j(Sl),d=j(Pl),b=j(Ut),x=j(()=>{const E=[{key:"_select",label:"",kind:"meta",sticky:"left"},{key:"case_id",label:"Case",kind:"meta",sticky:"left"},{key:"status",label:"Status",kind:"meta",sticky:"left"},...e(o).has("note")?[{key:"note",label:"Note",kind:"meta"}]:[]],R=e(i).map(B=>({key:B,label:B,kind:"doe"})),D=[{key:"nprocs",label:"MPI Ranks",kind:"calc"},{key:"nt",label:"Thread Count",kind:"calc"},{key:"last_iter",label:"Last Iter",kind:"calc"},{key:"duration",label:"Duration",kind:"calc"},{key:"last_mod",label:"Last Modified",kind:"calc"},{key:"resu_size_mb",label:"RESU Size (MB)",kind:"calc"}].filter(B=>e(o).has(B.key)),v=cr("gui")?[{key:"_actions",label:"",kind:"meta",sticky:"right"}]:[];return[...E,...R,...D,...v]});function A(E){const R=e(l).findIndex(D=>D.key===E);if(R<0)return"";const H=e(l)[R].dir==="asc"?"▲":"▼";return e(l).length>1?`${H}${R+1}`:H}function m(E,R){E!=="_actions"&&Il(E,R.ctrlKey||R.metaKey)}function N(E,R){R.shiftKey?sn(nn()||E,E,Rr()):R.ctrlKey||R.metaKey?an(E):Sa(E)}function L(E,R){var D;const H=((D=E.status)==null?void 0:D.toUpperCase())??"";H!=="DONE"&&H!=="FAILED"||(R.preventDefault(),r.onContextMenu(E.case_id,R.clientX,R.clientY,E.convergence??""))}async function g(E){const R=E.note??"",H=await hn("Case note:",R,`Note — ${E.case_id}`,"",!0);if(H!==null)try{await bi(E.case_id,H)}catch(D){await We(`Failed to set note: ${D instanceof Error?D.message:D}`,"Error")}}async function c(E){try{await yi(E)}catch(R){await We(`Failed to open GUI: ${R instanceof Error?R.message:R}`,"Error")}}function w(E,R){const H=(E==null?void 0:E.toUpperCase())??"";return(H==="DONE"||H==="FAILED")&&R==="converged"?"CONVERGED":(H==="DONE"||H==="FAILED")&&R==="not_converged"?"NOT CONVERGED":H}function P(E,R){const H=(E==null?void 0:E.toUpperCase())??"";return(H==="DONE"||H==="FAILED")&&R==="converged"?"status-converged":(H==="DONE"||H==="FAILED")&&R==="not_converged"?"status-not-converged":H==="RUNNING"?"status-running":H==="DONE"?"status-done":H==="FAILED"?"status-failed":H==="PREPARED"?"status-prepared":"status-unknown"}function h(E){return e(b).has(E.case_id)?"row-selected":""}function y(E,R){var H;return R.key==="case_id"?E.case_id:R.key==="nprocs"?E.nprocs!=null?String(E.nprocs):"":R.key==="nt"?E.nt!=null?String(E.nt):"":R.key==="last_iter"?E.last_iter!=null?String(E.last_iter):"":R.key==="duration"?E.duration??"":R.key==="last_mod"?C(E.last_mod):R.key==="resu_size_mb"?E.resu_size_mb!=null?String(E.resu_size_mb):"":R.kind==="doe"?((H=E.doe)==null?void 0:H[R.key])!=null&&String(E.doe[R.key])!==""?String(E.doe[R.key]):"—":""}function C(E){if(!E)return"";try{const R=new Date(E),H=new Date,D=R.toDateString()===H.toDateString(),v=new Date(H);v.setDate(v.getDate()-1);const B=R.toDateString()===v.toDateString(),X=R.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});return D?`Today ${X}`:B?`Yesterday ${X}`:R.toLocaleDateString([],{month:"short",day:"numeric"})+` ${X}`}catch{return E}}let _=z(ie({})),M=z(void 0);Re(()=>{e(d),e(x),r.rows,!(!e(M)||!e(d))&&Pt().then(()=>{if(!e(M))return;const E=Array.from(e(M).querySelectorAll("thead th.sticky-left")),R={};let H=0;E.forEach(D=>{const v=D.dataset.colKey??"";R[v]=H,H+=D.getBoundingClientRect().width}),f(_,R,!0)})});function F(E,R,H){const D=H&&E.key!=="_select"&&E.key!=="_actions"?["status-sortable"]:[];if(!H&&E.key==="case_id"&&D.push("case-id"),E.kind==="doe"&&D.push("status-col-doe"),!H&&E.key==="note"&&D.push("max-w-[140px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer hover:underline hover:decoration-dotted hover:underline-offset-2"),e(d)&&E.sticky==="left"&&D.push("sticky-left"),e(d)&&E.sticky==="right"&&D.push("sticky-right"),e(d)&&E.sticky==="left"){const v=e(x)[R+1];(!v||v.sticky!=="left")&&D.push("sticky-divider")}if(e(d)&&E.sticky==="right"){const v=e(x)[R-1];(!v||v.sticky!=="right")&&D.push("sticky-divider")}return D.join(" ")}function T(E,R){return F(E,R,!0)}function q(E,R){return F(E,R,!1)}function I(E){return!e(d)||!E.sticky?"":E.sticky==="right"?"position:sticky;right:0;":E.sticky==="left"&&E.key in e(_)?`position:sticky;left:${e(_)[E.key]}px;`:""}var O=ic(),V=k(O),G=k(V),Z=k(G);Ye(Z,23,()=>e(x),E=>E.key,(E,R,H)=>{var D=be(),v=K(D);{var B=fe=>{var W=Xl(),te=k(W),se=k(te);It(se,{get checked(){return e(a)},get indeterminate(){return e(s)},get onchange(){return r.onToggleAll},size:14}),p(te),p(W),J((de,ce)=>{Se(W,1,`${de??""} !px-2.5 !py-0 w-9`),we(W,"data-col-key",e(R).key),Ze(W,ce)},[()=>T(e(R),e(H)),()=>I(e(R))]),u(fe,W)},X=fe=>{var W=Yl();J((te,se)=>{Se(W,1,te),we(W,"data-col-key",e(R).key),Ze(W,se)},[()=>dt(T(e(R),e(H))),()=>I(e(R))]),u(fe,W)},ue=fe=>{var W=Zl(),te=k(W),se=$(te);{var de=re=>{var xe=Ql(),he=k(xe,!0);p(xe),J(Ee=>ne(he,Ee),[()=>A(e(R).key)]),u(re,xe)},ce=j(()=>A(e(R).key));Q(se,re=>{e(ce)&&re(de)})}p(W),J((re,xe)=>{Se(W,1,re),we(W,"data-col-key",e(R).key),Ze(W,xe),ne(te,`${e(R).label??""} `)},[()=>dt(T(e(R),e(H))),()=>I(e(R))]),le("click",W,re=>m(e(R).key,re)),le("keydown",W,re=>{(re.key==="Enter"||re.key===" ")&&(re.preventDefault(),m(e(R).key,re))}),u(fe,W)};Q(v,fe=>{e(R).key==="_select"?fe(B):e(R).key==="_actions"?fe(X,1):fe(ue,-1)})}u(E,D)}),p(Z),p(G);var ae=$(G);Ye(ae,21,()=>r.rows,E=>E.case_id,(E,R)=>{var H=oc();Ye(H,23,()=>e(x),D=>D.key,(D,v,B)=>{var X=be(),ue=K(X);{var fe=ce=>{var re=Jl(),xe=k(re),he=k(xe);{let Ee=j(()=>e(b).has(e(R).case_id));It(he,{get checked(){return e(Ee)},onchange:()=>r.onToggleRow(e(R).case_id),size:14})}p(xe),p(re),J((Ee,pe)=>{Se(re,1,`${Ee??""} !px-2.5 !py-0 w-9`),we(re,"data-col-key",e(v).key),Ze(re,pe)},[()=>q(e(v),e(B)),()=>I(e(v))]),u(ce,re)},W=ce=>{var re=ec(),xe=k(re),he=k(xe,!0);p(xe),p(re),J((Ee,pe,Fe,ze)=>{Se(re,1,Ee),we(re,"data-col-key",e(v).key),Ze(re,pe),Se(xe,1,`status-pill ${Fe??""}`),ne(he,ze)},[()=>dt(q(e(v),e(B))),()=>I(e(v)),()=>P(e(R).status,e(R).convergence),()=>w(e(R).status,e(R).convergence)]),u(ce,re)},te=ce=>{var re=rc(),xe=k(re);{var he=pe=>{var Fe=Ve();J(()=>ne(Fe,e(R).note)),u(pe,Fe)},Ee=pe=>{var Fe=tc(),ze=k(Fe);Te(ze,{get icon(){return qo},size:12}),p(Fe),u(pe,Fe)};Q(xe,pe=>{e(R).note?pe(he):pe(Ee,-1)})}p(re),J((pe,Fe)=>{Se(re,1,pe),we(re,"data-col-key",e(v).key),Ze(re,Fe),we(re,"title",e(R).note||"Add note")},[()=>dt(q(e(v),e(B))),()=>I(e(v))]),le("click",re,pe=>{pe.stopPropagation(),g(e(R))}),le("keydown",re,pe=>{(pe.key==="Enter"||pe.key===" ")&&(pe.preventDefault(),pe.stopPropagation(),g(e(R)))}),u(ce,re)},se=ce=>{var re=ac(),xe=k(re);$e(xe,{variant:"primary",size:"sm",onclick:he=>{he.stopPropagation(),c(e(R).case_id)},children:(he,Ee)=>{var pe=nc(),Fe=K(pe);Te(Fe,{get icon(){return Vo},size:12}),ke(),u(he,pe)},$$slots:{default:!0}}),p(re),J((he,Ee)=>{Se(re,1,he),we(re,"data-col-key",e(v).key),Ze(re,Ee)},[()=>dt(q(e(v),e(B))),()=>I(e(v))]),u(ce,re)},de=ce=>{var re=sc(),xe=k(re,!0);p(re),J((he,Ee,pe)=>{Se(re,1,he),we(re,"data-col-key",e(v).key),Ze(re,Ee),ne(xe,pe)},[()=>dt(q(e(v),e(B))),()=>I(e(v)),()=>y(e(R),e(v))]),u(ce,re)};Q(ue,ce=>{e(v).key==="_select"?ce(fe):e(v).key==="status"?ce(W,1):e(v).key==="note"?ce(te,2):e(v).key==="_actions"?ce(se,3):ce(de,-1)})}u(D,X)}),p(H),J(D=>Se(H,1,D),[()=>dt(h(e(R)))]),le("click",H,D=>N(e(R).case_id,D)),le("contextmenu",H,D=>L(e(R),D)),u(E,H)}),p(ae),p(V),nt(V,E=>f(M,E),()=>e(M)),p(O),J(()=>Se(O,1,`table-wrap ${e(d)?"sticky-enabled":""}`)),u(t,O),Ae()}Ke(["click","keydown","contextmenu"]);var cc=S('
        ');function uc(t,r){Me(r,!0);let a=j(()=>{const m=Math.min(r.x,window.innerWidth-180),N=Math.min(r.y,window.innerHeight-120);return`left: ${m}px; top: ${N}px;`});async function n(m){try{for(const N of r.cases)await xi(N,m)}catch(N){console.error("Failed to set convergence:",N)}r.onClose()}const s=r.cases.length>1?` (${r.cases.length})`:"";var i=cc();tt("keydown",it,m=>{m.key==="Escape"&&r.onClose()});var o=k(i),l=k(o);p(o);var d=$(o,2),b=k(d);p(d);var x=$(d,2),A=k(x);p(x),p(i),J(()=>{Ze(i,e(a)),Se(o,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue==="converged"?"is-active":""}`),ne(l,`Mark Converged${s}`),Se(d,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue==="not_converged"?"is-active":""}`),ne(b,`Mark Not Converged${s}`),Se(x,1,`w-full text-left py-[7px] px-[10px] bg-transparent border border-transparent rounded-md text-[12.5px] cursor-pointer transition-[background] duration-[120ms] ease-in-out hover:bg-edf-gris-clair hover:border-border ${r.currentValue===""?"is-active":""}`),ne(A,`Clear Mark${s}`)}),le("click",o,()=>n("converged")),le("click",d,()=>n("not_converged")),le("click",x,()=>n("")),u(t,i),Ae()}Ke(["click"]);var dc=S(" Refresh",1),fc=S(" ",1),vc=S(" Run",1),gc=S(" Restart",1),hc=S(" Stop",1),pc=S(" Kill",1),mc=S(" Clean",1),_c=S('
        ',1),bc=S('
        ',1);function xc(t,r){Me(r,!0);function a(){return[...Ut()]}async function n(){const v=a();if(!v.length)return;const B=await Xs(v);if(B)try{await On({cases:v,n:B.n,nt:B.nt,maxParallel:B.maxParallel}),r.onRefresh()}catch(X){await We(`Run failed: ${X instanceof Error?X.message:X}`,"Error")}}async function s(){const v=a();if(!v.length)return;const B=await Ys(v);if(B)try{await On({cases:v,n:B.n,nt:B.nt,maxParallel:B.maxParallel,restart:!0,restartMode:B.restartMode,restartValue:B.restartValue}),r.onRefresh()}catch(X){await We(`Restart failed: ${X instanceof Error?X.message:X}`,"Error")}}async function i(){const v=a();if(!(!v.length||!await Dn(`Kill ${v.length} case${v.length>1?"s":""}?`,"Confirm Kill","Kill","danger")))try{await mi(v),r.onRefresh()}catch(X){await We(`Kill failed: ${X instanceof Error?X.message:X}`,"Error")}}async function o(){const v=a();if(!(!v.length||!await Dn(`Stop ${v.length} case${v.length>1?"s":""} gracefully (checkpoint + exit)?`,"Confirm Stop","Stop","danger")))try{await kr({cases:v,action:"stop"}),r.onRefresh(),Cr(`Stop requested for ${v.length} case${v.length>1?"s":""} — will checkpoint and exit`)}catch(X){await We(`Stop failed: ${X instanceof Error?X.message:X}`,"Error")}}async function l(){const v=a();if(v.length)try{await kr({cases:v,action:"checkpoint"}),r.onRefresh(),Cr(`Checkpoint requested for ${v.length} case${v.length>1?"s":""}`)}catch(B){await We(`Checkpoint failed: ${B instanceof Error?B.message:B}`,"Error")}}async function d(){const v=a();if(!v.length)return;const B=await hn("Extend by how many additional time steps?","500","Extend Run");if(B===null)return;const X=parseInt(B,10);if(!Number.isFinite(X)||X<=0){await We("Enter a positive integer number of time steps.","Error");return}try{await kr({cases:v,action:"extend",value:X}),r.onRefresh(),Cr(`Extended ${v.length} case${v.length>1?"s":""} by ${X} time steps`)}catch(ue){await We(`Extend failed: ${ue instanceof Error?ue.message:ue}`,"Error")}}async function b(){const v=a();if(v.length)try{await kr({cases:v,action:"flush"}),Cr(`Flush requested for ${v.length} case${v.length>1?"s":""}`)}catch(B){await We(`Flush failed: ${B instanceof Error?B.message:B}`,"Error")}}async function x(){const v=a();if(!v.length)return;const B=await Qs(v);if(B)try{await _i({cases:v,keepLast:B.keepLast,keepResu:B.keepResu,deleteResu:B.deleteResu,pruneResu:!0}),r.onRefresh(),Wi()}catch(X){await We(`Cleanup failed: ${X instanceof Error?X.message:X}`,"Error")}}function A(){const v=Rr();v.length>0&&v.every(X=>Ut().has(X))?Gn():Kn(v)}function m(v){an(v)}let N=z(ie(tr("status")));Re(()=>{rr("status",e(N))}),Ll(!0);let L=j(()=>{const v=Sr();return v.size>0&&[...v].some(B=>B==="PREPARED"||B==="DONE"||B==="FAILED")}),g=j(()=>{if(!cr("restart"))return!1;const v=Sr();return v.size>0&&[...v].some(B=>B==="DONE"||B==="FAILED")&&Xn()}),c=j(()=>{const v=Sr();return v.size>0&&v.has("RUNNING")}),w=j(()=>{if(!cr("control"))return!1;const v=Sr();return v.size>0&&v.has("RUNNING")});const P=[{label:"Extend",icon:Uo,onClick:d,action:"extend"},{label:"Checkpoint",icon:Xo,onClick:l,action:"checkpoint"},{label:"Flush",icon:Ho,onClick:b,action:"flush"}];let h=j(()=>P.filter(v=>nl(v.action)).map(v=>({label:v.label,icon:v.icon,onClick:v.onClick,disabled:!e(w)}))),y=j(Xn),C=z(ie($l())),_=null;function M(){_&&clearTimeout(_),_=setTimeout(()=>{Al(e(C))},200)}Re(()=>Tl(e(N)));let F=z(!1),T=z(0),q=z(0),I=z(ie([])),O=z("");function V(v,B,X,ue){f(I,Ut().has(v)?[...Ut()]:[v],!0),f(O,ue,!0),f(T,B,!0),f(q,X,!0),f(F,!0)}function G(){f(F,!1)}function Z(v){const B=Rr();if(B.length){if(v.key==="Escape"){Gn(),G();return}if((v.ctrlKey||v.metaKey)&&v.key==="a"){v.preventDefault(),Kn(B);return}if(v.key==="ArrowDown"||v.key==="ArrowUp"){v.preventDefault();const X=v.key==="ArrowDown"?1:-1,ue=Un(),fe=B.indexOf(ue),W=Math.max(0,Math.min(B.length-1,fe+X)),te=B[W];v.shiftKey?sn(nn()||B[0],te,B):Sa(te),Ml(te);return}if(v.key===" "){v.preventDefault();const X=Un();X&&(v.shiftKey?sn(nn()||X,X,Rr()):an(X))}}}var ae=bc();tt("click",it,G),tt("scroll",it,G);var E=K(ae),R=k(E);Ht(R,{eyebrow:"Overview",title:"Status",wide:!0,id:"status-card-shell",actions:B=>{var X=fc(),ue=K(X);{let te=j(Ki);nr(ue,{name:"status",get intervalMs(){return e(te)},get onRefresh(){return r.onRefresh},get checked(){return e(N)},set checked(se){f(N,se,!0)}})}var fe=$(ue,2);{var W=te=>{$e(te,{variant:"primary",onclick:()=>r.onRefresh(),children:(se,de)=>{var ce=dc(),re=K(ce);Te(re,{get icon(){return er}}),ke(),u(se,ce)},$$slots:{default:!0}})};Q(fe,te=>{e(N)||te(W)})}u(B,X)},children:(B,X)=>{var ue=_c(),fe=K(ue),W=k(fe),te=k(W);Xe(te);var se=$(te,2);Wl(se,{}),p(W);var de=$(W,2),ce=k(de),re=k(ce);p(ce);var xe=$(ce,2);{let U=j(()=>!e(L));$e(xe,{variant:"run",size:"sm",onclick:n,get disabled(){return e(U)},children:(ee,oe)=>{var ve=vc(),ye=K(ve);Te(ye,{get icon(){return _a}}),ke(),u(ee,ve)},$$slots:{default:!0}})}var he=$(xe,2);{var Ee=U=>{{let ee=j(()=>!e(g));$e(U,{variant:"warning",size:"sm",onclick:s,get disabled(){return e(ee)},children:(oe,ve)=>{var ye=gc(),ge=K(ye);Te(ge,{get icon(){return Wo}}),ke(),u(oe,ye)},$$slots:{default:!0}})}},pe=j(()=>cr("restart"));Q(he,U=>{e(pe)&&U(Ee)})}var Fe=$(he,2);{var ze=U=>{{let ee=j(()=>!e(w));$e(U,{variant:"warning",size:"sm",onclick:o,get disabled(){return e(ee)},children:(oe,ve)=>{var ye=hc(),ge=K(ye);Te(ge,{get icon(){return Io}}),ke(),u(oe,ye)},$$slots:{default:!0}})}},Qe=j(()=>cr("control"));Q(Fe,U=>{e(Qe)&&U(ze)})}var je=$(Fe,2);{var rt=U=>{xl(U,{get items(){return e(h)}})};Q(je,U=>{e(h).length>0&&U(rt)})}var Le=$(je,2);{let U=j(()=>!e(c));$e(Le,{variant:"danger",size:"sm",onclick:i,get disabled(){return e(U)},children:(ee,oe)=>{var ve=pc(),ye=K(ve);Te(ye,{get icon(){return Oo}}),ke(),u(ee,ve)},$$slots:{default:!0}})}var Pe=$(Le,2);{let U=j(()=>!e(y));$e(Pe,{variant:"secondary",size:"sm",onclick:x,get disabled(){return e(U)},children:(ee,oe)=>{var ve=mc(),ye=K(ve);Te(ye,{get icon(){return Qo}}),ke(),u(ee,ve)},$$slots:{default:!0}})}p(de),p(fe);var Y=$(fe,2);{let U=j(Pa);lc(Y,{get rows(){return e(U)},onContextMenu:V,onToggleAll:A,onToggleRow:m})}J(U=>ne(re,`${U??""} selected`),[()=>Ut().size]),le("input",te,M),et(te,()=>e(C),U=>f(C,U)),u(B,ue)},$$slots:{actions:!0,default:!0}}),p(E);var H=$(E,2);{var D=v=>{uc(v,{get cases(){return e(I)},get x(){return e(T)},get y(){return e(q)},get currentValue(){return e(O)},onClose:G})};Q(H,v=>{e(F)&&v(D)})}le("keydown",E,Z),u(t,ae),Ae()}Ke(["keydown","input"]);var yc=S('
        ');function Qt(t,r){var a=yc(),n=k(a);ft(n,()=>r.children),p(a),u(t,a)}function Cn(t){return t.map(r=>({value:r,label:r}))}var wc=S(''),kc=S(" ",1);function Sn(t,r){Me(r,!0);let a=me(r,"columnLabel",3,"Columns"),n=me(r,"xMinLabel",3,"Iter min"),s=j(()=>Cn(r.allCases)),i=j(()=>r.columns.map(d=>({value:d,label:d}))),o=[{value:"zero",label:"Zero"},{value:"restart",label:"Restart start"},{value:"custom",label:"Custom"}];function l(d){r.onXMinChange(Number(d.target.value)||0)}Qt(t,{children:(d,b)=>{var x=kc(),A=K(x);Ne(A,{text:"Cases",children:(y,C)=>{Gt(y,{class:"w-[160px]",get options(){return e(s)},get selected(){return r.selectedCases},get onchange(){return r.onCasesChange},placeholder:"Select cases..."})}});var m=$(A,2);{var N=y=>{var C=be(),_=K(C);ft(_,()=>r.middleSlot),u(y,C)};Q(m,y=>{r.middleSlot&&y(N)})}var L=$(m,2);Ne(L,{get text(){return a()},children:(y,C)=>{Gt(y,{class:"w-[160px]",get options(){return e(i)},get selected(){return r.selectedColumns},get onchange(){return r.onColumnsChange},placeholder:"Select..."})}});var g=$(L,2);Ne(g,{text:"Start from",children:(y,C)=>{lt(y,{class:"w-[140px]",get options(){return o},get value(){return r.startFrom},onchange:_=>r.onStartFromChange(_)})}});var c=$(g,2);{var w=y=>{Ne(y,{get text(){return n()},children:(C,_)=>{var M=wc();Xe(M),J(()=>vn(M,r.xMin)),le("input",M,l),u(C,M)}})};Q(c,y=>{r.startFrom==="custom"&&y(w)})}var P=$(c,2);{var h=y=>{var C=be(),_=K(C);ft(_,()=>r.extraSlot),u(y,C)};Q(P,y=>{r.extraSlot&&y(h)})}u(d,x)}}),Ae()}Ke(["input"]);var Cc=S('
        '),Sc=S('
        ');function Zt(t,r){let a=me(r,"emptyMessage",3,"");var n=Sc(),s=k(n);{var i=l=>{var d=be(),b=K(d);zr(b,()=>r.svgHtml),u(l,d)},o=l=>{var d=Cc(),b=k(d),x=k(b,!0);p(b),p(d),J(()=>ne(x,a()||"No data to display.")),u(l,d)};Q(s,l=>{r.svgHtml?l(i):l(o,-1)})}p(n),J(()=>we(n,"id",r.id)),u(t,n)}async function $n(t,r="plot.png"){const a=document.getElementById(t),n=a==null?void 0:a.querySelector("svg");if(!n)return;const{width:s,height:i}=$c(n),o=window.devicePixelRatio||1,l=document.createElement("canvas");l.width=s*o,l.height=i*o;const d=l.getContext("2d");if(!d)return;d.scale(o,o);const b=new XMLSerializer().serializeToString(n),x=new Blob([b],{type:"image/svg+xml;charset=utf-8"}),A=URL.createObjectURL(x),m=new Image;m.width=s,m.height=i,await new Promise((L,g)=>{m.onload=()=>{d.drawImage(m,0,0,s,i),URL.revokeObjectURL(A),L()},m.onerror=g,m.src=A});const N=await new Promise(L=>l.toBlob(L,"image/png"));N&&await Ra(N,Pc(r))}function xr(t,r,a="png"){const n=r.length<=3?r.join("_"):`${r.length}_cases`,s=new Date().toISOString().replace(/[:.]/g,"-").slice(0,19);return Ec(`${t}_${n}_${s}.${a}`)}async function Ea(t,r){const a=new Blob([t],{type:"text/csv;charset=utf-8"});await Ra(a,r)}function $c(t){const r=t.getAttribute("viewBox");if(r){const a=r.split(/[\s,]+/).map(Number);if(a.length===4)return{width:a[2],height:a[3]}}return{width:t.width.baseVal.value||900,height:t.height.baseVal.value||500}}async function Ra(t,r){if("showSaveFilePicker"in window)try{const i=await(await window.showSaveFilePicker({suggestedName:r,types:[{description:t.type.startsWith("image/")?"Image":"File",accept:{[t.type]:[`.${r.split(".").pop()}`]}}]})).createWritable();await i.write(t),await i.close();return}catch(s){if((s==null?void 0:s.name)==="AbortError")return}const a=URL.createObjectURL(t),n=document.createElement("a");n.href=a,n.download=r,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(a)}function Pc(t){return t.endsWith(".png")?t:`${t}.png`}function Ec(t){return t.replace(/[^a-zA-Z0-9._-]/g,"_")}let Na=z(ie([])),Ma=z(ie([])),Aa=z(ie([])),La=z("zero"),Ta=z(0),Fa=z(""),Rc=z(!0);function Nc(){return{selectedCases:e(Na),columns:e(Ma),selectedColumns:e(Aa),startFrom:e(La),iterMin:e(Ta),svgHtml:e(Fa),autoRefresh:e(Rc)}}function Yn(t){f(Na,t,!0)}function Mc(t){f(Ma,t,!0)}function Xr(t){f(Aa,t,!0)}function Ac(t){f(La,t,!0)}function Lc(t){f(Ta,t,!0)}function $r(t){f(Fa,t,!0)}var Tc=S(" Refresh",1),Fc=S(" ",1),Dc=S(" Download as PNG",1),zc=S('
        '),Ic=S('
        ',1),Oc=S('
        '),Hc=S('

        '),jc=S(" ",1);function Vc(t,r){Me(r,!0);let a=j(Nc),n=z("");const s=new Set(["iteration","wall_distance","walldistance"]),i=["velocity","pressure"];async function o(){if(e(a).selectedCases.length)try{const g=await ii(e(a).selectedCases);f(n,"");const c=g.filter(w=>!s.has(w.toLowerCase().replace(/\s+/g,"_")));if(Mc(c),c.length===0){Xr([]),$r("");return}if(e(a).selectedColumns.length===0){const w=c.filter(P=>i.some(h=>P.toLowerCase().includes(h)));Xr(w.length>0?w:[c[0]])}}catch(g){f(n,"Failed to load residual columns"),console.error("Failed to load residual columns:",g)}}async function l(){if(e(a).selectedCases.length&&!((e(a).columns.length===0||e(a).selectedColumns.length===0)&&(await o(),!e(a).selectedColumns.length)))try{let g=0,c=!0;if(e(a).startFrom==="restart"){const P=await wa(e(a).selectedCases),h=Object.values(P.origins).map(y=>y.iteration).filter(y=>y!==void 0&&Number.isFinite(y));g=h.length>0?Math.min(...h):0}else e(a).startFrom==="custom"&&(g=e(a).iterMin);const w=await li(e(a).selectedCases,e(a).selectedColumns,{xMin:g,includeHistory:c});f(n,""),w&&$r(w)}catch(g){f(n,"Failed to load residual plot"),console.error("Failed to load residual plot:",g)}}function d(g){if(Yn(g),g.length===0){$r("");return}o(),l()}function b(g){if(Xr(g),g.length===0){$r("");return}l()}function x(g){Ac(g),l()}function A(g){Lc(g),l()}let m=z(ie(tr("plot")));Re(()=>{rr("plot",e(m))}),Re(()=>{e(m)?mr("residualPlot",l,jn()):vt("residualPlot")});let N=!1;Re(()=>{r.allCases.length>0&&!N&&(N=!0,Yn([...r.allCases]),o().then(()=>l()))});const L=_r(()=>{N&&o().then(()=>l())});vr(()=>{vt("residualPlot"),L()}),Ht(t,{eyebrow:"Diagnostics",title:"Residuals Plot",wide:!0,actions:c=>{var w=Fc(),P=K(w);{let C=j(jn);nr(P,{name:"residualPlot",get intervalMs(){return e(C)},onRefresh:l,get checked(){return e(m)},set checked(_){f(m,_,!0)}})}var h=$(P,2);{var y=C=>{$e(C,{variant:"primary",onclick:l,children:(_,M)=>{var F=Tc(),T=K(F);Te(T,{get icon(){return er}}),ke(),u(_,F)},$$slots:{default:!0}})};Q(h,C=>{e(m)||C(y)})}u(c,w)},children:(c,w)=>{var P=jc(),h=K(P);{var y=F=>{const T=G=>{var Z=zc(),ae=k(Z);{let E=j(()=>!e(a).svgHtml);$e(ae,{variant:"secondary",size:"sm",onclick:()=>$n("plot-holder",xr("residuals",e(a).selectedCases)),get disabled(){return e(E)},children:(R,H)=>{var D=Dc(),v=K(D);Te(v,{get icon(){return pr}}),ke(),u(R,D)},$$slots:{default:!0}})}p(Z),u(G,Z)};var q=Ic(),I=K(q);Sn(I,{prefix:"plot",get allCases(){return r.allCases},get selectedCases(){return e(a).selectedCases},onCasesChange:d,columnLabel:"Variables",get columns(){return e(a).columns},get selectedColumns(){return e(a).selectedColumns},onColumnsChange:b,get startFrom(){return e(a).startFrom},onStartFromChange:x,xMinLabel:"Iter min",get xMin(){return e(a).iterMin},onXMinChange:A,get extraSlot(){return T}});var O=$(I,2),V=k(O);{let G=j(()=>e(a).selectedCases.length===0?"Please select at least one case.":e(a).selectedColumns.length===0?"Please select at least one variable.":"No data to display.");Zt(V,{id:"plot-holder",get svgHtml(){return e(a).svgHtml},get emptyMessage(){return e(G)}})}p(O),u(F,q)},C=F=>{var T=Oc(),q=k(T);Zt(q,{id:"plot-holder",svgHtml:"",emptyMessage:"No data available. Please run a simulation first."}),p(T),u(F,T)};Q(h,F=>{e(a).columns.length>0?F(y):F(C,-1)})}var _=$(h,2);{var M=F=>{var T=Hc(),q=k(T,!0);p(T),J(()=>ne(q,e(n))),u(F,T)};Q(_,F=>{e(n)&&F(M)})}u(c,P)},$$slots:{actions:!0,default:!0}}),Ae()}function Da(t){const{scope:r,getState:a,setState:n,setHasData:s,getAxis:i}=t;async function o(){const m=a();if(m.selectedCases.length)try{let N=await Jr(m.selectedCases,r);if(t.filterFiles&&(N=t.filterFiles(N)),n({files:N}),s(N.length>0),N.length===0){n({file:"",columns:[],selectedColumns:[],positionText:"",svgHtml:""});return}(!m.file||!N.includes(m.file))&&(n({file:N[0]}),await l())}catch(N){console.error(`Failed to load ${r} files:`,N)}}async function l(){const m=a();if(!(!m.selectedCases.length||!m.file))try{const N=await vi(m.selectedCases,[m.file]),L=t.filterColumns?t.filterColumns(N):N;t.onColumnsLoaded?t.onColumnsLoaded(N):n({columns:L});const g=a();if(g.columns.length===0){n({selectedColumns:[],positionText:"",svgHtml:""});return}g.selectedColumns.length===0&&n({selectedColumns:[g.columns[0]]})}catch(N){console.error(`Failed to load ${r} columns:`,N)}}async function d(){let m=a();if(m.selectedCases.length){if(await o(),m=a(),!m.file){n({positionText:"",svgHtml:""});return}if((m.columns.length===0||m.selectedColumns.length===0)&&(await l(),m=a(),!m.selectedColumns.length)){n({positionText:"",svgHtml:""});return}try{let N;const L=a();if(L.startFrom==="restart"){const c=await wa(L.selectedCases),w=i().toLowerCase(),P=w==="t"||w==="time"?"time":"iteration",h=Object.values(c.origins).map(y=>y[P]).filter(y=>y!==void 0&&Number.isFinite(y));N=h.length>0?Math.min(...h):void 0}else L.startFrom==="custom"&&(N=L.xMin);const g=await hi({cases:L.selectedCases,probes:[L.file],columns:L.selectedColumns,axis:i(),timeMin:typeof N=="number"?N:void 0,xMin:N,includeHistory:!0});g&&n({svgHtml:g})}catch(N){console.error(`Failed to load ${r} plot:`,N)}}}async function b(m){if(n({selectedCases:m}),m.length===0){n({svgHtml:""});return}await o(),r==="monitoring"&&await d()}function x(m){if(n({selectedColumns:m}),m.length===0){n({svgHtml:""});return}d()}async function A(m){n({file:m}),await l(),await d()}return{loadFiles:o,loadColumns:l,loadPlot:d,handleCasesChange:b,handleColumnsChange:x,handleFileChange:A}}function za(){return{selectedCases:[],file:"",files:[],columns:[],selectedColumns:[],axis:"time",startFrom:"zero",xMin:0,positionText:"",svgHtml:"",autoRefresh:!0}}let Bt=z("time"),on=z(ie(za())),ln=z(ie({...za(),axis:""})),Uc=z(!0),Tr=z(!1),Fr=z(!1);function Bc(){return e(Bt)}function qc(t){f(Bt,t,!0)}function Kc(){return e(Tr)}function cn(t){f(Tr,t,!0)}function Gc(){return e(Fr)}function un(t){f(Fr,t,!0)}function Wc(){e(Bt)==="time"&&!e(Tr)&&e(Fr)?f(Bt,"profile"):e(Bt)==="profile"&&!e(Fr)&&e(Tr)&&f(Bt,"time")}function Qn(){return e(on)}function Yr(){return e(ln)}function kt(t){f(on,{...e(on),...t},!0)}function At(t){f(ln,{...e(ln),...t},!0)}function Xc(){return e(Uc)}var Yc=S(" Download as PNG",1),Qc=S('
        '),Zc=S("
        "),Jc=S('
        '),eu=S('
        ',1),tu=S('
        ');function ru(t,r){Me(r,!0);let a=me(r,"onRefresh",15),n=j(Qn);const s=new Set(["time","t","iteration","iter"]),i=Da({scope:"monitoring",getState:Qn,setState:kt,setHasData:cn,filterFiles:g=>g.filter(c=>{const w=c.replace(/\.csv$/i,"").toLowerCase();return w!=="coords"&&!w.includes("coordinates")}),filterColumns:g=>g.filter(c=>!s.has(c.toLowerCase())),onColumnsLoaded:g=>{const c=g.filter(w=>!s.has(w.toLowerCase()));kt({columns:c}),o()},getAxis:()=>"time"});async function o(){if(!(!e(n).selectedCases.length||!e(n).file||!e(n).selectedColumns.length))try{const g=[];for(const C of e(n).selectedColumns){const _=await gi(e(n).selectedCases[0],e(n).file,[C]);_.found&&g.push({col:C,x:_.x,y:_.y,z:_.z})}if(!g.length){kt({positionText:""});return}const c=C=>{let _=0;for(const M of C){if(M===void 0)continue;const F=String(M),T=F.indexOf(".");T>=0&&(_=Math.max(_,F.length-T-1))}return _},w=c(g.map(C=>C.x)),P=c(g.map(C=>C.y)),h=c(g.map(C=>C.z)),y=g.map(C=>{const _=[];return C.x!==void 0&&_.push(`x=${C.x.toFixed(w)}`),C.y!==void 0&&_.push(`y=${C.y.toFixed(P)}`),C.z!==void 0&&_.push(`z=${C.z.toFixed(h)}`),`Probe position [${C.col}] : ${_.join(" · ")}`});kt({positionText:y.join(` -`)})}catch{kt({positionText:""})}}function l(g){i.handleColumnsChange(g),g.length>0&&o()}let d=j(()=>e(n).files.map(g=>({value:g,label:g.replace(/\.csv$/i,"").replace(/^probes?_/i,"")}))),b=!1;Re(()=>{r.allCases.length>0&&!b&&(b=!0,kt({selectedCases:[...r.allCases]}),i.loadFiles().then(()=>i.loadPlot()))}),Re(()=>{a(i.loadPlot)});let x=j(()=>e(n).selectedCases.length===0?"Please select at least one case.":e(n).selectedColumns.length===0?"Please select at least one probe.":"No data to display.");var A=be(),m=K(A);{var N=g=>{const c=F=>{Ne(F,{text:"Quantity",children:(T,q)=>{lt(T,{class:"w-[160px]",get options(){return e(d)},get value(){return e(n).file},get onchange(){return i.handleFileChange}})}})},w=F=>{var T=Qc(),q=k(T);{let I=j(()=>!e(n).svgHtml);$e(q,{variant:"secondary",size:"sm",onclick:()=>$n("probe-plot-holder",xr("probe",e(n).selectedCases)),get disabled(){return e(I)},children:(O,V)=>{var G=Yc(),Z=K(G);Te(Z,{get icon(){return pr}}),ke(),u(O,G)},$$slots:{default:!0}})}p(T),u(F,T)};var P=eu(),h=K(P);Sn(h,{prefix:"probe",get allCases(){return r.allCases},get selectedCases(){return e(n).selectedCases},get onCasesChange(){return i.handleCasesChange},get middleSlot(){return c},columnLabel:"Probes",get columns(){return e(n).columns},get selectedColumns(){return e(n).selectedColumns},onColumnsChange:l,get startFrom(){return e(n).startFrom},onStartFromChange:F=>{kt({startFrom:F}),i.loadPlot()},xMinLabel:"Time min",get xMin(){return e(n).xMin},onXMinChange:F=>{kt({xMin:F}),i.loadPlot()},get extraSlot(){return w}});var y=$(h,2);{var C=F=>{var T=Jc();Ye(T,21,()=>e(n).positionText.split(` -`).sort(),fn,(q,I)=>{var O=Zc(),V=k(O,!0);p(O),J(()=>ne(V,e(I))),u(q,O)}),p(T),u(F,T)};Q(y,F=>{e(n).positionText&&F(C)})}var _=$(y,2),M=k(_);Zt(M,{id:"probe-plot-holder",get svgHtml(){return e(n).svgHtml},get emptyMessage(){return e(x)}}),p(_),u(g,P)},L=g=>{var c=tu(),w=k(c);Zt(w,{id:"probe-plot-holder",svgHtml:"",emptyMessage:"No probe data available. Please run a simulation first."}),p(c),u(g,c)};Q(m,g=>{e(n).files.length>0?g(N):g(L,-1)})}u(t,A),Ae()}var nu=S(" ",1),au=S(" Download as PNG",1),su=S('
        '),ou=S('
        ',1),iu=S('
        ');function lu(t,r){Me(r,!0);let a=me(r,"onRefresh",15),n=j(Yr);const s=["s","x","abscissa","distance","arclength","arc_length","curvilinear","length","r","y","z","coord","position"];function i(c){const w=c.map(P=>P.toLowerCase());for(const P of s){const h=w.indexOf(P);if(h>=0)return c[h]}return c[0]??""}const o=Da({scope:"profiles",getState:Yr,setState:At,setHasData:un,onColumnsLoaded:c=>{const w=e(n).axis||i(c),P=c.filter(h=>h!==w).toSorted((h,y)=>h.localeCompare(y));At({columns:P,axis:w,selectedColumns:e(n).selectedColumns.length?e(n).selectedColumns:P.length?[P[0]]:[]})},getAxis:()=>Yr().axis});function l(c){const w=e(n).axis;At({axis:c});const h=[...e(n).columns,...w?[w]:[]].filter(y=>y!==c).toSorted((y,C)=>y.localeCompare(C));At({columns:h}),o.loadPlot()}let d=j(()=>e(n).files.map(c=>({value:c,label:c.replace(/\.csv$/i,"").replace(/^profiles\//i,"").replace(/_/g," ")}))),b=j(()=>[...e(n).axis?[e(n).axis]:[],...e(n).columns].toSorted((c,w)=>c.localeCompare(w)).map(c=>({value:c,label:c}))),x=!1;Re(()=>{r.allCases.length>0&&!x&&(x=!0,At({selectedCases:[...r.allCases]}),o.loadFiles().then(()=>o.loadPlot()))}),Re(()=>{a(o.loadPlot)});let A=j(()=>e(n).selectedCases.length===0?"Please select at least one case.":e(n).selectedColumns.length===0?"Please select at least one value.":"No data to display.");var m=be(),N=K(m);{var L=c=>{const w=M=>{var F=nu(),T=K(F);Ne(T,{text:"Profile",children:(I,O)=>{lt(I,{class:"w-[160px]",get options(){return e(d)},get value(){return e(n).file},get onchange(){return o.handleFileChange}})}});var q=$(T,2);Ne(q,{text:"X axis",children:(I,O)=>{lt(I,{class:"w-[140px]",get options(){return e(b)},get value(){return e(n).axis},onchange:l})}}),u(M,F)},P=M=>{var F=su(),T=k(F);{let q=j(()=>!e(n).svgHtml);$e(T,{variant:"secondary",size:"sm",onclick:()=>$n("profile-plot-holder",xr("profile",e(n).selectedCases)),get disabled(){return e(q)},children:(I,O)=>{var V=au(),G=K(V);Te(G,{get icon(){return pr}}),ke(),u(I,V)},$$slots:{default:!0}})}p(F),u(M,F)};var h=ou(),y=K(h);Sn(y,{prefix:"profile",get allCases(){return r.allCases},get selectedCases(){return e(n).selectedCases},get onCasesChange(){return o.handleCasesChange},get middleSlot(){return w},columnLabel:"Values",get columns(){return e(n).columns},get selectedColumns(){return e(n).selectedColumns},get onColumnsChange(){return o.handleColumnsChange},get startFrom(){return e(n).startFrom},onStartFromChange:M=>{At({startFrom:M}),o.loadPlot()},xMinLabel:"X min",get xMin(){return e(n).xMin},onXMinChange:M=>{At({xMin:M}),o.loadPlot()},get extraSlot(){return P}});var C=$(y,2),_=k(C);Zt(_,{id:"profile-plot-holder",get svgHtml(){return e(n).svgHtml},get emptyMessage(){return e(A)}}),p(C),u(c,h)},g=c=>{var w=iu(),P=k(w);Zt(P,{id:"profile-plot-holder",svgHtml:"",emptyMessage:"No profile data available. Please run a simulation first."}),p(w),u(c,w)};Q(N,c=>{e(n).files.length>0?c(L):c(g,-1)})}u(t,m),Ae()}var cu=S('
        Diagnostics
        '),uu=S(" Refresh",1),du=S(" ",1);function fu(t,r){Me(r,!0);let a=j(Bc),n=j(Xc),s=j(Kc),i=j(Gc),o=z(ie(tr("probe")));Re(()=>{rr("probe",e(o))});let l=z(void 0);function d(){var L;(L=e(l))==null||L()}async function b(){if(r.allCases.length){try{const g=(await Jr(r.allCases,"monitoring")).filter(c=>{const w=c.replace(/\.csv$/i,"").toLowerCase();return w!=="coords"&&!w.includes("coordinates")});cn(g.length>0)}catch{cn(!1)}try{const L=await Jr(r.allCases,"profiles");un(L.length>0)}catch{un(!1)}}}Re(()=>{r.allCases.length>0&&b()}),_r(()=>{b(),d()}),Re(()=>{e(s),e(i),Wc()});function x(L){L==="time"&&!e(s)||L==="profile"&&!e(i)||qc(L)}var A=be(),m=K(A);{var N=L=>{{const g=w=>{var P=cu(),h=$(k(P),2),y=k(h),C=$(y,2);p(h),p(P),J(()=>{Se(y,1,`bg-transparent border-none p-0 m-0 text-lg font-bold tracking-tight transition-colors duration-150 - ${e(s)?e(a)==="time"?"text-edf-bleu-fonce cursor-pointer":"text-edf-gris-moyen cursor-pointer hover:text-muted":"text-edf-gris-moyen cursor-not-allowed opacity-50"}`),y.disabled=!e(s),we(y,"title",e(s)?"":"No probe data available. Run a simulation first."),Se(C,1,`bg-transparent border-none p-0 m-0 text-lg font-bold tracking-tight transition-colors duration-150 - ${e(i)?e(a)==="profile"?"text-edf-bleu-fonce cursor-pointer":"text-edf-gris-moyen cursor-pointer hover:text-muted":"text-edf-gris-moyen cursor-not-allowed opacity-50"}`),C.disabled=!e(i),we(C,"title",e(i)?"":"No profile data available. Run a simulation first.")}),le("click",y,()=>x("time")),le("click",C,()=>x("profile")),u(w,P)};Ht(L,{wide:!0,titleSlot:g,tabs:g,actions:w=>{var P=du(),h=K(P);{let _=j(()=>e(a)==="time"?"probePlot":"profilePlot"),M=j(Gi);nr(h,{get name(){return e(_)},get intervalMs(){return e(M)},onRefresh:d,get checked(){return e(o)},set checked(F){f(o,F,!0)}})}var y=$(h,2);{var C=_=>{$e(_,{variant:"primary",onclick:d,children:(M,F)=>{var T=uu(),q=K(T);Te(q,{get icon(){return er}}),ke(),u(M,T)},$$slots:{default:!0}})};Q(y,_=>{e(o)||_(C)})}u(w,P)},children:(w,P)=>{var h=be(),y=K(h);{var C=M=>{ru(M,{get allCases(){return r.allCases},get onRefresh(){return e(l)},set onRefresh(F){f(l,F,!0)}})},_=M=>{lu(M,{get allCases(){return r.allCases},get onRefresh(){return e(l)},set onRefresh(F){f(l,F,!0)}})};Q(y,M=>{e(a)==="time"?M(C):M(_,-1)})}u(w,h)},$$slots:{tabs:!0,actions:!0,default:!0}})}};Q(m,L=>{e(n)&&L(N)})}u(t,A),Ae()}Ke(["click"]);var vu=S(" Refresh",1),gu=S(" ",1),hu=S(" Download as CSV",1),pu=S('
        ',1),mu=S(" "),_u=S(' '),bu=S(' '),xu=S('
        Case
        '),yu=S('

        '),wu=S(" ",1);function ku(t,r){Me(r,!0);let a=z(ie([])),n=z(ie([])),s=z(!1),i=z(!1),o=z(ie(tr("perf")));Re(()=>{rr("perf",e(o))});const l=5e3;let b=z(ie([{key:"elapsed_time",label:"Elapsed (s)",kind:"time"},{key:"io_time",label:"I/O (s)",kind:"time"},{key:"linear_solver_time",label:"Linear Solver (s)",kind:"time"},{key:"gradients_time",label:"Gradients (s)",kind:"time"},{key:"balances_time",label:"Balances (s)",kind:"time"},{key:"mpi_ranks",label:"MPI Ranks",kind:"int"},{key:"threads",label:"Threads",kind:"int"}])),x=j(()=>r.allCases.map(P=>({value:P,label:P}))),A=j(()=>e(n).length>0);async function m(){var P;if(e(a).length){f(i,!0);try{const h=await In(e(a));(P=h.columns)!=null&&P.length&&f(b,h.columns,!0),f(n,h.records,!0),e(n).length>0&&f(s,!0)}catch(h){console.error("Failed to load perf:",h)}f(i,!1)}}function N(P){if(f(a,P,!0),P.length===0){f(n,[],!0);return}m()}function L(P,h){if(h==null||h==="")return"-";if(P==="text")return h;const y=Number(h);return Number.isFinite(y)?P==="int"?String(Math.round(y)):y.toFixed(3):h}async function g(){if(!e(n).length)return;const P=["case_id",...e(b).map(_=>_.key)],h=e(n).map(_=>[_.case_id,...e(b).map(M=>_[M.key]??"")].join(",")),y=[P.join(","),...h].join(` -`),C=xr("timing_snapshot",e(a),"csv");await Ea(y,C)}let c=!1;Re(()=>{r.allCases.length>0&&!c&&(c=!0,f(a,[...r.allCases],!0),In(r.allCases).then(P=>{var h;(h=P.columns)!=null&&h.length&&f(b,P.columns,!0),f(s,P.records.length>0),f(n,P.records,!0)}).catch(()=>{}))}),Re(()=>{e(o)?mr("perf",m,l):vt("perf")});const w=_r(()=>{c&&m()});vr(()=>{vt("perf"),w()}),Ht(t,{eyebrow:"Performance",title:"Timing Snapshot",wide:!0,actions:h=>{var y=gu(),C=K(y);nr(C,{name:"perf",intervalMs:l,onRefresh:m,get checked(){return e(o)},set checked(F){f(o,F,!0)}});var _=$(C,2);{var M=F=>{$e(F,{variant:"primary",onclick:m,children:(T,q)=>{var I=vu(),O=K(I);Te(O,{get icon(){return er}}),ke(),u(T,I)},$$slots:{default:!0}})};Q(_,F=>{e(o)||F(M)})}u(h,y)},children:(h,y)=>{var C=wu(),_=K(C);{var M=I=>{Qt(I,{children:(O,V)=>{var G=pu(),Z=K(G);Ne(Z,{text:"Cases",children:(R,H)=>{Gt(R,{class:"w-[160px]",get options(){return e(x)},get selected(){return e(a)},onchange:N,placeholder:"Select cases..."})}});var ae=$(Z,2),E=k(ae);{let R=j(()=>!e(A));$e(E,{variant:"secondary",size:"sm",onclick:g,get disabled(){return e(R)},children:(H,D)=>{var v=hu(),B=K(v);Te(B,{get icon(){return pr}}),ke(),u(H,v)},$$slots:{default:!0}})}p(ae),u(O,G)}})};Q(_,I=>{e(s)&&I(M)})}var F=$(_,2);{var T=I=>{var O=xu(),V=k(O),G=k(V),Z=k(G),ae=$(k(Z));Ye(ae,17,()=>e(b),R=>R.key,(R,H)=>{var D=mu(),v=k(D,!0);p(D),J(()=>ne(v,e(H).label)),u(R,D)}),p(Z),p(G);var E=$(G);Ye(E,21,()=>e(n),R=>R.case_id,(R,H)=>{var D=bu(),v=k(D),B=k(v,!0);p(v);var X=$(v);Ye(X,17,()=>e(b),ue=>ue.key,(ue,fe)=>{var W=_u(),te=k(W,!0);p(W),J(se=>ne(te,se),[()=>L(e(fe).kind,e(H)[e(fe).key])]),u(ue,W)}),p(D),J(()=>ne(B,e(H).case_id)),u(R,D)}),p(E),p(V),p(O),u(I,O)},q=I=>{var O=yu(),V=k(O);{var G=ae=>{var E=Ve("Select cases above to view timing information.");u(ae,E)},Z=ae=>{var E=Ve("No performance data available. Please run a simulation first.");u(ae,E)};Q(V,ae=>{e(s)?ae(G):ae(Z,-1)})}p(O),u(I,O)};Q(F,I=>{e(A)?I(T):I(q,-1)})}u(h,C)},$$slots:{actions:!0,default:!0}}),Ae()}const Cu={"&":"&","<":"<",">":">",'"':""","'":"'"};function Ia(t){return t.replace(/[&<>"']/g,r=>Cu[r]??r)}var Su=S("
        "),$u=S('
        ');function Pu(t,r){Me(r,!0);let a=z(void 0);function n(b){const x=Ia(b);if(!r.searchQuery.trim())return x;try{const A=new RegExp(`(${r.searchQuery})`,"gi");return x.replace(A,'$1')}catch{return x}}function s(b){return b==="error"?"tail-sev-error":b==="warn"?"tail-sev-warn":b==="info"?"tail-sev-info":""}function i(){return e(a)?e(a).scrollHeight-e(a).scrollTop-e(a).clientHeight<=24:!0}let o=z(!0);function l(){f(o,i(),!0)}Re(()=>{r.autoScroll&&r.lines.length>0&&e(a)&&e(o)&&Pt().then(()=>{e(a)&&(e(a).scrollTop=e(a).scrollHeight)})});var d=$u();Ye(d,21,()=>r.lines,b=>b.index,(b,x)=>{var A=Su();zr(A,()=>n(e(x).text),!0),p(A),J(m=>Se(A,1,`tail-line ${m??""} ${e(x).isNew?"tail-new":""}`),[()=>s(e(x).severity)]),u(b,A)}),p(d),nt(d,b=>f(a,b),()=>e(a)),tt("scroll",d,l),u(t,d),Ae()}const Eu=/(fatal error|error detected|error reading|error writing|segmentation fault|sigterm|sigsegv|sigkill|abort|killed|terminated|core dumped|traceback|exception|errno)/i,Ru=/(warning|divergence|non-convergence|clipping|negative|nan detected|overflow|underflow)/i;function Nu(t){return Eu.test(t)?"error":Ru.test(t)?"warn":"none"}function Mu(t,r){return r==="all"||r===""?!0:r==="info"?t!=="none":t===r}var Au=S(" Refresh",1),Lu=S(" ",1),Tu=S(''),Fu=S(''),Du=S("    ",1),zu=Ot(''),Iu=Ot(''),Ou=Ot(''),Hu=S(" Resume",1),ju=S(" Pause",1),Vu=S('Case not running'),Uu=S(' 
        ',1),Bu=S('

        No log data available. Please run a simulation first.

        '),qu=S('

        '),Ku=S(" ",1);function Gu(t,r){Me(r,!0);const a=80;let n=z(""),s=z(""),i=z(!1),o=z(ie([])),l=z(a),d=z(ie([])),b=z(ie(new Set)),x=z(!1),A=z(ie(tr("tail")));Re(()=>{rr("tail",e(A))});let m=z(!0),N=z("all"),L=z(""),g=z(""),c=z(void 0),w=j(()=>Cn(r.allCases)),P=j(()=>e(o).map(E=>{const R=E.split("/").pop()??E;return{value:E,label:R}})),h=[{value:"all",label:"All"},{value:"error",label:"Error"},{value:"warn",label:"Warn"}],y=j(()=>e(o).length>0),C=j(()=>yn().some(E=>{var R;return E.case_id===e(n)&&((R=E.status)==null?void 0:R.toUpperCase())==="RUNNING"}));const _={"run_solver.log":1,listing:2,"run_status.running":3,"csauto.stdout":4,"csauto.stderr":5,"performance.log":6};function M(E){const R=E.split("/").pop()??E;return _[R]??99}async function F(){if(e(n))try{const H=(await di(e(n))).filter(D=>{const v=D.split("/").pop()??D;return D.endsWith(".log")||D.endsWith("/summary")||v in _}).sort((D,v)=>M(D)-M(v));if(f(o,H,!0),H.length>0){const D=H[0];!e(s)||!H.includes(e(s))?(f(s,D,!0),f(i,!1)):!e(i)&&M(D)0&&H[H.length-1]===""?H.slice(0,-1):H;if(e(d).length>0){const v=q(e(d),D),B=new Set;for(let X=v;X{let E=e(d).map((R,H)=>({text:R,index:H,severity:Nu(R),isNew:e(b).has(H)}));if(e(N)!=="all"&&(E=E.filter(R=>Mu(R.severity,e(N)))),e(L).trim())try{const R=new RegExp(e(L),"i");E=E.filter(H=>R.test(H.text))}catch{}return E});function O(E){f(n,E,!0),f(s,""),f(i,!1),f(d,[],!0),F().then(()=>T(!0))}function V(E){f(s,E,!0),f(i,!0),f(d,[],!0),T(!0)}function G(){f(x,!e(x)),e(x)||T(!0)}function Z(E){(E.ctrlKey||E.metaKey)&&E.key==="f"&&e(c)&&(E.preventDefault(),e(c).focus())}Re(()=>{r.allCases.length>0&&!e(n)&&(f(n,r.allCases[0],!0),F().then(()=>T(!0)))}),Re(()=>{e(A)&&!e(x)&&e(n)?mr("tail",()=>T(!1),Hn()):vt("tail")});const ae=_r(()=>{e(n)&&F().then(()=>T(!0))});vr(()=>{vt("tail"),ae()}),tt("keydown",it,Z),Ht(t,{eyebrow:"Live",title:"Log Tail",wide:!0,actions:R=>{var H=Lu(),D=K(H);{let X=j(Hn);nr(D,{name:"tail",get intervalMs(){return e(X)},onRefresh:()=>T(!1),get checked(){return e(A)},set checked(ue){f(A,ue,!0)}})}var v=$(D,2);{var B=X=>{$e(X,{variant:"primary",onclick:()=>T(!0),children:(ue,fe)=>{var W=Au(),te=K(W);Te(te,{get icon(){return er}}),ke(),u(ue,W)},$$slots:{default:!0}})};Q(v,X=>{e(A)||X(B)})}u(R,H)},children:(R,H)=>{var D=Ku(),v=K(D);{var B=se=>{Qt(se,{children:(de,ce)=>{var re=Du(),xe=K(re);Ne(xe,{text:"Case",children:(ze,Qe)=>{lt(ze,{class:"w-[130px]",get options(){return e(w)},get value(){return e(n)},onchange:O})}});var he=$(xe,2);Ne(he,{text:"File",children:(ze,Qe)=>{lt(ze,{class:"w-[150px]",get options(){return e(P)},get value(){return e(s)},onchange:V})}});var Ee=$(he,2);Ne(Ee,{text:"Lines",children:(ze,Qe)=>{var je=Tu();Xe(je),le("change",je,()=>T(!0)),et(je,()=>e(l),rt=>f(l,rt)),u(ze,je)}});var pe=$(Ee,2);Ne(pe,{text:"Filter",children:(ze,Qe)=>{var je=Fu();Xe(je),nt(je,rt=>f(c,rt),()=>e(c)),et(je,()=>e(L),rt=>f(L,rt)),u(ze,je)}});var Fe=$(pe,2);Ne(Fe,{text:"Severity",children:(ze,Qe)=>{lt(ze,{class:"w-[90px]",get options(){return h},get value(){return e(N)},onchange:je=>f(N,je,!0)})}}),u(de,re)}})};Q(v,se=>{e(y)&&se(B)})}var X=$(v,2);{var ue=se=>{var de=Uu(),ce=K(de);Pu(ce,{get lines(){return e(I)},get searchQuery(){return e(L)},get autoScroll(){return e(m)}});var re=$(ce,2),xe=k(re),he=k(xe);{var Ee=Y=>{var U=zu();u(Y,U)},pe=Y=>{var U=Iu();u(Y,U)},Fe=Y=>{var U=Ou();u(Y,U)};Q(he,Y=>{e(x)?Y(Ee):e(C)?Y(pe,1):Y(Fe,-1)})}var ze=$(he);p(xe);var Qe=$(xe,2);{var je=Y=>{$e(Y,{variant:"secondary",size:"sm",onclick:G,children:(U,ee)=>{var oe=be(),ve=K(oe);{var ye=_e=>{var Ce=Hu(),Ie=K(Ce);Te(Ie,{get icon(){return _a}}),ke(),u(_e,Ce)},ge=_e=>{var Ce=ju(),Ie=K(Ce);Te(Ie,{get icon(){return Bo}}),ke(),u(_e,Ce)};Q(ve,_e=>{e(x)?_e(ye):_e(ge,-1)})}u(U,oe)},$$slots:{default:!0}})},rt=Y=>{var U=Vu();u(Y,U)};Q(Qe,Y=>{e(C)?Y(je):Y(rt,-1)})}var Le=$(Qe,2),Pe=k(Le);It(Pe,{get checked(){return e(m)},onchange:Y=>f(m,Y,!0),size:14,label:"Auto-scroll",labelFirst:!0}),p(Le),p(re),J(()=>ne(ze,` ${e(I).length??""} / ${e(d).length??""}`)),u(se,de)},fe=se=>{var de=Bu();u(se,de)};Q(X,se=>{e(d).length>0?se(ue):se(fe,-1)})}var W=$(X,2);{var te=se=>{var de=qu(),ce=k(de,!0);p(de),J(()=>ne(ce,e(g))),u(se,de)};Q(W,se=>{e(g)&&se(te)})}u(R,D)},$$slots:{actions:!0,default:!0}}),Ae()}Ke(["change"]);function Wu(t,r=3){var N,L;const a=t.split(` -`),n=[];let s=0,i=0,o=!1;for(const g of a){if(g.startsWith("===")||g.startsWith("---")||g.startsWith("+++"))continue;const c=g.match(/^@@ -(\d+),?\d* \+(\d+),?\d* @@/);if(c){s=parseInt(c[1],10)-1,i=parseInt(c[2],10)-1,o=!0;continue}if(o)if(g.startsWith("-"))s++,n.push({type:"del",leftNum:s,rightNum:null,leftContent:g.slice(1),rightContent:""});else if(g.startsWith("+"))i++,n.push({type:"add",leftNum:null,rightNum:i,leftContent:"",rightContent:g.slice(1)});else{s++,i++;const w=g.startsWith(" ")?g.slice(1):g;n.push({type:"equal",leftNum:s,rightNum:i,leftContent:w,rightContent:w})}}const l=[];let d=0;for(;d=0&&g-A>1&&x.push({kind:"separator",skipped:g-A-1});const c=l[g].type!=="equal",w=c&&!m;x.push({kind:"line",row:l[g],hunkStart:w}),m=c,A=g}if(A=0&&x.push({kind:"separator",skipped:l.length-1-A}),x.length>0&&x[0].kind==="line"){const g=((N=x[0].row)==null?void 0:N.leftNum)??0,c=((L=x[0].row)==null?void 0:L.rightNum)??0,w=Math.max(g,c)-1;w>0&&x.unshift({kind:"separator",skipped:w})}return x}var Xu=S('
        '),Yu=S('
        '),Qu=S(' ',1),Zu=S('No matches'),Ju=S('
        '),ed=S('

        Files are identical.

        '),td=S('
        Loading...
        '),rd=S('
        ');function nd(t,r){Me(r,!0);let a=me(r,"loading",3,!1),n=z(void 0),s=j(()=>Wu(r.diffText)),i=j(()=>{const Y=[];return e(s).forEach((U,ee)=>{U.hunkStart&&Y.push(ee)}),Y}),o=j(()=>e(i).length>0),l=j(()=>{var U;const Y=new Array(e(s).length).fill(-1);for(let ee=0;ee{r.diffText!==b&&(b=r.diffText,f(d,-1))});let x=j(()=>{if(!r.search.trim())return[];try{const Y=new RegExp(r.search,"i"),U=[];return e(s).forEach((ee,oe)=>{ee.kind==="line"&&ee.row&&(Y.test(ee.row.leftContent)||Y.test(ee.row.rightContent))&&U.push(oe)}),U}catch{return[]}}),A=z(-1),m="";Re(()=>{r.search!==m&&(m=r.search,f(A,e(x).length>0?0:-1,!0))});async function N(Y){var oe;f(A,Y,!0),await Pt();const U=e(x)[Y],ee=(oe=e(h))==null?void 0:oe.querySelector(`[data-item-idx="${U}"]`);if(ee&&e(h)&&e(y)){const ve=e(h).getBoundingClientRect(),ge=ee.getBoundingClientRect().top-ve.top+e(h).scrollTop-e(h).clientHeight/3;_=!0,e(h).scrollTop=ge,e(y).scrollTop=ge,requestAnimationFrame(()=>{_=!1})}}function L(){e(A)>0&&N(e(A)-1)}function g(){e(A)<0&&e(x).length>0?N(0):e(A)$1`)}catch{return ee}}function w(Y,U){return Y==="equal"?"":Y==="del"&&U==="left"?"bg-[rgba(214,67,10,0.08)] text-edf-orange-fonce":Y==="del"&&U==="right"?"bg-[rgba(48,122,16,0.08)] text-edf-vert-fonce":Y==="add"&&U==="left"?"bg-edf-gris-clair":Y==="add"&&U==="right"?"bg-[rgba(48,122,16,0.08)] text-edf-vert-fonce":""}function P(Y,U){return Y==="del"&&U==="left"?"rgb(251,237,233)":Y==="del"&&U==="right"?"rgb(238,247,236)":Y==="add"&&U==="left"?"var(--color-edf-gris-clair)":Y==="add"&&U==="right"?"rgb(238,247,236)":"white"}let h=z(void 0),y=z(void 0),C=!1,_=!1;function M(Y){C||_||!e(h)||!e(y)||(C=!0,Y==="left"?(e(y).scrollTop=e(h).scrollTop,e(y).scrollLeft=e(h).scrollLeft):(e(h).scrollTop=e(y).scrollTop,e(h).scrollLeft=e(y).scrollLeft),requestAnimationFrame(()=>{C=!1}))}async function F(Y){var ee;f(d,Y,!0),await Pt();const U=(ee=e(h))==null?void 0:ee.querySelector(`[data-hunk-start="${Y}"]`);if(U&&e(h)&&e(y)){_=!0;const oe=e(h).getBoundingClientRect(),ye=U.getBoundingClientRect().top-oe.top+e(h).scrollTop-e(h).clientHeight/3;e(h).scrollTop=ye,e(y).scrollTop=ye,requestAnimationFrame(()=>{_=!1})}}function T(){e(d)>0&&F(e(d)-1)}function q(){e(d)<0?F(0):e(d)=0&&f(d,U,!0)}function O(Y){Y.target instanceof HTMLInputElement||(Y.key==="n"&&!Y.shiftKey?(Y.preventDefault(),q()):Y.key==="N"||Y.key==="n"&&Y.shiftKey?(Y.preventDefault(),T()):Y.key==="j"?(Y.preventDefault(),g()):(Y.key==="J"||Y.key==="j"&&Y.shiftKey)&&(Y.preventDefault(),L()))}var V=rd();{const Y=(U,ee=ws)=>{var oe=be(),ve=K(oe);Ye(ve,17,()=>e(s),fn,(ye,ge,_e)=>{const Ce=j(()=>e(i).indexOf(_e)),Ie=j(()=>e(l)[_e]>=0&&e(l)[_e]===e(d)),Ge=j(()=>{var at,qe;return ee()==="left"?(at=e(ge).row)==null?void 0:at.leftNum:(qe=e(ge).row)==null?void 0:qe.rightNum}),gt=j(()=>{var at,qe;return ee()==="left"?((at=e(ge).row)==null?void 0:at.leftContent)??"":((qe=e(ge).row)==null?void 0:qe.rightContent)??""});var jt=be(),jr=K(jt);{var Je=at=>{var qe=Xu(),Nt=$(k(qe),2),Vr=k(Nt);p(Nt),p(qe),J(()=>ne(Vr,`... ${e(ge).skipped??""} lines hidden ...`)),u(at,qe)},ut=at=>{var qe=Yu(),Nt=k(qe),Vr=k(Nt,!0);p(Nt);var Pn=$(Nt,2);zr(Pn,()=>c(e(gt),e(A)>=0&&e(x)[e(A)]===_e),!0),p(Pn),p(qe),J((yt,Mt,yr,Oa,Ha)=>{Se(qe,1,`flex w-full ${yt??""} ${Mt??""}`),Ze(qe,e(Ie)?"background-color: rgba(16,87,200,0.12); color: rgb(16,87,200);":""),we(qe,"role",yr),we(qe,"tabindex",Oa),we(qe,"data-hunk-start",ee()==="left"&&e(Ce)>=0?e(Ce):void 0),we(qe,"data-item-idx",ee()==="left"?_e:void 0),Ze(Nt,`background: ${Ha??""}; color: ${e(Ie)?"white":"var(--color-muted)"};`),ne(Vr,e(Ge)??"")},[()=>w(e(ge).row.type,ee()),()=>e(ge).row.type!=="equal"||e(x).includes(_e)?"cursor-pointer":"",()=>e(ge).row.type!=="equal"||e(x).includes(_e)?"button":void 0,()=>e(ge).row.type!=="equal"||e(x).includes(_e)?0:void 0,()=>e(Ie)?"rgb(16,87,200)":P(e(ge).row.type,ee())]),le("click",qe,()=>{var Mt;((Mt=e(ge).row)==null?void 0:Mt.type)!=="equal"&&I(_e);const yt=e(x).indexOf(_e);yt>=0&&f(A,yt,!0)}),le("keydown",qe,yt=>{var Mt;if(yt.key==="Enter"||yt.key===" "){yt.preventDefault(),((Mt=e(ge).row)==null?void 0:Mt.type)!=="equal"&&I(_e);const yr=e(x).indexOf(_e);yr>=0&&f(A,yr,!0)}}),u(at,qe)};Q(jr,at=>{e(ge).kind==="separator"?at(Je):e(ge).row&&at(ut,1)})}u(ye,jt)}),u(U,oe)};var G=k(V),Z=k(G),ae=k(Z),E=k(ae,!0);p(ae);var R=$(ae,2),H=k(R,!0);p(R),p(Z);var D=$(Z,2),v=k(D),B=k(v,!0);p(v);var X=$(v,2),ue=k(X,!0);p(X),p(D),p(G);var fe=$(G,2),W=k(fe),te=k(W);Xe(te);var se=$(te,2);{var de=U=>{var ee=Qu(),oe=K(ee);{let _e=j(()=>e(A)<=0);$e(oe,{variant:"secondary",size:"sm",onclick:L,get disabled(){return e(_e)},children:(Ce,Ie)=>{ke();var Ge=Ve("Prev");u(Ce,Ge)},$$slots:{default:!0}})}var ve=$(oe,2),ye=k(ve);p(ve);var ge=$(ve,2);{let _e=j(()=>e(A)>=e(x).length-1);$e(ge,{variant:"secondary",size:"sm",onclick:g,get disabled(){return e(_e)},children:(Ce,Ie)=>{ke();var Ge=Ve("Next");u(Ce,Ge)},$$slots:{default:!0}})}J(()=>ne(ye,`${e(A)>=0?e(A)+1:"–"} / ${e(x).length??""} occurrence${e(x).length!==1?"s":""}`)),u(U,ee)},ce=j(()=>r.search.trim()&&e(x).length>0),re=U=>{var ee=Zu();u(U,ee)},xe=j(()=>r.search.trim());Q(se,U=>{e(ce)?U(de):e(xe)&&U(re,1)})}p(W);var he=$(W,2),Ee=k(he);{let U=j(()=>e(i).length===0||e(d)<=0);$e(Ee,{variant:"secondary",size:"sm",onclick:T,get disabled(){return e(U)},children:(ee,oe)=>{ke();var ve=Ve("Prev");u(ee,ve)},$$slots:{default:!0}})}var pe=$(Ee,2),Fe=k(pe);p(pe);var ze=$(pe,2);{let U=j(()=>e(i).length===0||e(d)>=e(i).length-1);$e(ze,{variant:"secondary",size:"sm",onclick:q,get disabled(){return e(U)},children:(ee,oe)=>{ke();var ve=Ve("Next");u(ee,ve)},$$slots:{default:!0}})}p(he),p(fe);var Qe=$(fe,2);{var je=U=>{var ee=Ju(),oe=k(ee),ve=k(oe),ye=k(ve);Y(ye,()=>"left"),p(ve),p(oe),nt(oe,Ie=>f(h,Ie),()=>e(h));var ge=$(oe,2),_e=k(ge),Ce=k(_e);Y(Ce,()=>"right"),p(_e),p(ge),nt(ge,Ie=>f(y,Ie),()=>e(y)),p(ee),tt("scroll",oe,()=>M("left")),tt("scroll",ge,()=>M("right")),u(U,ee)},rt=U=>{var ee=ed();u(U,ee)};Q(Qe,U=>{e(o)?U(je):U(rt,-1)})}var Le=$(Qe,2);{var Pe=U=>{var ee=td();u(U,ee)};Q(Le,U=>{a()&&U(Pe)})}p(V),nt(V,U=>f(n,U),()=>e(n)),J(()=>{ne(E,r.leftLabel),ne(H,r.kind),ne(B,r.rightLabel),ne(ue,r.kind),vn(te,r.search),ne(Fe,`${e(d)>=0?e(d)+1:"–"} / ${e(i).length??""} diff${e(i).length!==1?"s":""}`)}),le("input",te,U=>r.onSearchChange(U.target.value)),le("keydown",te,U=>{U.key==="Enter"&&(U.preventDefault(),g())})}le("keydown",V,O),u(t,V),Ae()}Ke(["keydown","click","input"]);var ad=S(" ",1),sd=S('

        Please select two different cases to compare.

        '),od=S('

        Select two cases to compare.

        '),id=S(' '),ld=S('
        Parameter
        '),cd=S('

        All parameters are identical.

        '),ud=S('
        ',1),dd=S('

        '),fd=S('

        Loading...

        '),vd=S(" ",1),gd=S(" ",1);function hd(t,r){Me(r,!0);let a=z(""),n=z(""),s=z(""),i=z(""),o=z(""),l=z(!1),d=z(!1),b=z(""),x=j(()=>r.allCases.map(_=>({value:_,label:_})));const A=[{value:"setup.xml",label:"setup.xml"},{value:"doe_row.csv",label:"doe_row.csv"},{value:"run_solver.log",label:"run_solver.log"},{value:"performance.log",label:"performance.log"}];let m=j(()=>{var _;return((_=Ft())==null?void 0:_.compare_kinds)??A});Re(()=>{var _;e(m).some(M=>M.value===e(s))||f(s,((_=e(m)[0])==null?void 0:_.value)??"",!0)});let N=z(!1),L=j(()=>e(a)&&e(n)&&e(a)!==e(n));function g(){const _=e(a);f(a,e(n),!0),f(n,_,!0)}async function c(){if(e(L)){f(l,!0),f(b,"");try{const _=await pi({cases:[e(a),e(n)],base:e(a),kind:e(s)});f(o,_,!0),f(d,!0)}catch(_){console.error("Failed to load diff:",_),f(o,""),f(d,!1);const M=_ instanceof Error?_.message:String(_);f(b,M.includes("404")?`File "${e(s)}" not found for one of the selected cases. It may not have been run yet.`:"Failed to load comparison.",!0)}f(l,!1)}}Re(()=>{e(a)&&e(n)&&e(a)!==e(n)&&e(s)?c():e(a)&&e(n)&&e(a)===e(n)&&(f(o,""),f(d,!0))});let w=!1;Re(()=>{r.allCases.length>=2&&!w?(w=!0,f(a,r.allCases[0],!0),f(n,r.allCases[1],!0)):r.allCases.length===1&&!w&&(w=!0,f(a,r.allCases[0],!0))});let P=j(wn),h=j(()=>{if(!e(a)||!e(n)||e(a)===e(n)||e(P).length===0)return[];const _=yn(),M=_.find(T=>T.case_id===e(a)),F=_.find(T=>T.case_id===e(n));return!M&&!F?[]:e(P).map(T=>{var O,V;const q=((O=M==null?void 0:M.doe)==null?void 0:O[T])!=null&&String(M.doe[T])!==""?String(M.doe[T]):"—",I=((V=F==null?void 0:F.doe)==null?void 0:V[T])!=null&&String(F.doe[T])!==""?String(F.doe[T]):"—";return{param:T,left:q,right:I,differs:q!==I}})}),y=j(()=>e(N)?e(h):e(h).filter(_=>_.differs)),C=j(()=>e(h).filter(_=>_.differs).length);Ht(t,{eyebrow:"Compare",title:"Side-by-Side Comparison",wide:!0,children:(_,M)=>{var F=gd(),T=K(F);Qt(T,{children:(G,Z)=>{var ae=ad(),E=K(ae);Ne(E,{text:"First case",children:(D,v)=>{lt(D,{class:"w-32.5",get options(){return e(x)},get value(){return e(a)},onchange:B=>f(a,B,!0),placeholder:"Select..."})}});var R=$(E,2);{let D=j(()=>!e(a)&&!e(n));$e(R,{variant:"secondary",size:"sm",onclick:g,get disabled(){return e(D)},children:(v,B)=>{Te(v,{get icon(){return Fo}})},$$slots:{default:!0}})}var H=$(R,2);Ne(H,{text:"Second case",children:(D,v)=>{lt(D,{class:"w-32.5",get options(){return e(x)},get value(){return e(n)},onchange:B=>f(n,B,!0),placeholder:"Select..."})}}),u(G,ae)}});var q=$(T,2);{var I=G=>{var Z=sd();u(G,Z)},O=G=>{var Z=od();u(G,Z)},V=G=>{var Z=vd(),ae=K(Z);{var E=X=>{var ue=ud(),fe=K(ue),W=k(fe),te=k(W);p(W);var se=$(W,2),de=k(se,!0);p(se),p(fe);var ce=$(fe,2);{var re=he=>{var Ee=ld(),pe=k(Ee),Fe=k(pe),ze=k(Fe),Qe=k(ze),je=k(Qe,!0);p(Qe);var rt=$(Qe,2),Le=k(rt,!0);p(rt),p(ze),p(Fe);var Pe=$(Fe);Ye(Pe,21,()=>e(y),Y=>Y.param,(Y,U)=>{var ee=id(),oe=k(ee),ve=k(oe,!0);p(oe);var ye=$(oe),ge=k(ye,!0);p(ye);var _e=$(ye),Ce=k(_e,!0);p(_e),p(ee),J(()=>{Se(oe,1,dt(e(U).differs?"text-edf-orange-fonce bg-[rgba(214,67,10,0.04)]":"")),ne(ve,e(U).left),ne(ge,e(U).param),Se(_e,1,dt(e(U).differs?"text-edf-vert-fonce bg-[rgba(48,122,16,0.04)]":"")),ne(Ce,e(U).right)}),u(Y,ee)}),p(Pe),p(pe),p(Ee),J(()=>{ne(je,e(a)),ne(Le,e(n))}),u(he,Ee)},xe=he=>{var Ee=cd();u(he,Ee)};Q(ce,he=>{e(y).length>0?he(re):he(xe,-1)})}J(()=>{ne(te,`${e(C)??""} difference${e(C)!==1?"s":""} out of ${e(h).length??""} - parameters`),ne(de,e(N)?"Show differences only":"Show all parameters")}),le("click",se,()=>f(N,!e(N))),u(X,ue)};Q(ae,X=>{e(h).length>0&&X(E)})}var R=$(ae,2);Qt(R,{children:(X,ue)=>{Ne(X,{text:"File",children:(fe,W)=>{lt(fe,{class:"w-37.5",get options(){return e(m)},get value(){return e(s)},onchange:te=>f(s,te,!0)})}})}});var H=$(R,2);{var D=X=>{var ue=dd(),fe=k(ue,!0);p(ue),J(()=>ne(fe,e(b))),u(X,ue)},v=X=>{nd(X,{get diffText(){return e(o)},get leftLabel(){return e(a)},get rightLabel(){return e(n)},get kind(){return e(s)},get search(){return e(i)},get loading(){return e(l)},onSearchChange:ue=>f(i,ue,!0)})},B=X=>{var ue=fd();u(X,ue)};Q(H,X=>{e(b)?X(D):e(d)?X(v,1):e(l)&&X(B,2)})}u(G,Z)};Q(q,G=>{e(a)&&e(n)&&e(a)===e(n)?G(I):!e(a)||!e(n)?G(O,1):G(V,-1)})}u(_,F)},$$slots:{default:!0}}),Ae()}Ke(["click"]);var pd=S(" Refresh",1),md=S(" ",1),_d=S(''),bd=S(''),xd=S(" Download as CSV",1),yd=S('
        ',1),wd=S(" ",1),kd=S(""),Cd=S('
        Sort by:
        '),Sd=S('

        No errors found. Select cases and adjust filters above.

        '),$d=S(' '),Pd=S('NEW'),Ed=S('
        '),Rd=S('
        '),Nd=S(' '),Md=S('
        '),Ad=S('

        '),Ld=S("
        ",1);function Td(t,r){Me(r,!0);const a=6,n=["csauto.stderr","run_solver.log","listing"];let s=z(ie([])),i=z(ie([...n])),o=!1;Re(()=>{var te;const W=(te=Ft())==null?void 0:te.error_files;W!=null&&W.length&&!o&&f(i,[...W],!0)});let l=z("all"),d=z(""),b=z(a),x=z(!1),A=z(""),m=z(ie(tr("errors")));Re(()=>{rr("errors",e(m))});let N=z(ie([])),L=z(ie(new Set));function g(W){return W.toLowerCase().replace(/[0-9]+/g,"#").replace(/0x[0-9a-f]+/gi,"#").replace(/\s+/g," ").trim()}function c(W){const te=W.line_html.replace(/<[^>]*>/g,"").slice(0,120);return`${W.case_id}|${W.file}|${W.severity}|${g(te)}`}function w(W){const te=new Map;for(const se of W){const de=c(se),ce=te.get(de);ce?ce.count++:te.set(de,{...se,count:1,fingerprint:de,isNew:!e(L).has(de)})}return Array.from(te.values())}async function P(){if(!(!e(s).length||!e(i).length)){f(x,!0);try{const W=await ci({cases:e(s),files:e(i),context:e(b),sev:e(l)==="all"?"":e(l),q:e(d)});f(A,"");const te=w(W.items);f(N,W.items,!0);const se=new Set(te.map(de=>de.fingerprint));f(L,se,!0),f(h,te,!0)}catch(W){f(A,"Failed to load errors"),console.error("Failed to load errors:",W)}finally{f(x,!1)}}}let h=z(ie([])),y=null;function C(){y&&clearTimeout(y),y=setTimeout(P,250)}function _(W){return W==="error"?"error-sev-error":W==="warn"?"error-sev-warn":"error-sev-info"}let M=j(()=>Cn(r.allCases));const F=["csauto.stderr","run_solver.log","listing","csauto.stdout"];let T=j(()=>{var W;return(((W=Ft())==null?void 0:W.error_files)??F).map(te=>({value:te,label:te}))}),q=[{value:"all",label:"All"},{value:"error",label:"Error"},{value:"warn",label:"Warn"},{value:"info",label:"Info"}];function I(W){if(f(s,W,!0),W.length===0){f(h,[],!0),f(N,[],!0);return}P()}function O(W){if(o=!0,f(i,W,!0),W.length===0){f(h,[],!0),f(N,[],!0);return}P()}function V(W){f(l,W,!0),P()}function G(){P()}let Z=!1;Re(()=>{r.allCases.length>0&&!Z&&(Z=!0,f(s,[...r.allCases],!0),P())}),Re(()=>{e(m)?mr("errors",P,Vn()):vt("errors")});const ae=_r(()=>{Z&&P()});vr(()=>{vt("errors"),ae()});let E=z(ie(new Set));function R(W){const te=new Set(e(E));te.has(W)?te.delete(W):te.add(W),f(E,te,!0)}let H=z("severity"),D=z("desc");const v={info:0,warn:1,error:2};let B=j(()=>{const W=[...e(h)];return W.sort((te,se)=>{let de=0;return e(H)==="severity"?de=(v[te.severity]??9)-(v[se.severity]??9):e(H)==="count"?de=te.count-se.count:de=te[e(H)].localeCompare(se[e(H)]),e(D)==="asc"?de:-de}),W});const X={severity:"desc",count:"desc",case_id:"asc",file:"asc"};function ue(W){e(H)===W?f(D,e(D)==="asc"?"desc":"asc",!0):(f(H,W,!0),f(D,X[W],!0))}function fe(){if(!e(h).length)return;const W=["case_id","file","severity","count","is_new","line_text"],te=e(B).map(ce=>[ce.case_id,ce.file,ce.severity,ce.count,ce.isNew?"yes":"no",`"${ce.line_html.replace(/<[^>]*>/g,"").replace(/"/g,'""')}"`].join(",")),se=[W.join(","),...te].join(` -`),de=xr("errors",e(s),"csv");Ea(se,de)}Ht(t,{eyebrow:"Diagnostics",title:"Recent Errors",wide:!0,actions:te=>{var se=md(),de=K(se);{let xe=j(Vn);nr(de,{name:"errors",get intervalMs(){return e(xe)},onRefresh:P,get checked(){return e(m)},set checked(he){f(m,he,!0)}})}var ce=$(de,2);{var re=xe=>{$e(xe,{variant:"primary",onclick:P,children:(he,Ee)=>{var pe=pd(),Fe=K(pe);Te(Fe,{get icon(){return er}}),ke(),u(he,pe)},$$slots:{default:!0}})};Q(ce,xe=>{e(m)||xe(re)})}u(te,se)},children:(te,se)=>{var de=Ld(),ce=K(de);Qt(ce,{children:(Le,Pe)=>{var Y=yd(),U=K(Y);Ne(U,{text:"Cases",children:(Ce,Ie)=>{Gt(Ce,{class:"w-[160px]",get options(){return e(M)},get selected(){return e(s)},onchange:I,placeholder:"Select cases..."})}});var ee=$(U,2);Ne(ee,{text:"Files",children:(Ce,Ie)=>{Gt(Ce,{class:"w-[160px]",get options(){return e(T)},get selected(){return e(i)},onchange:O,placeholder:"Select files..."})}});var oe=$(ee,2);Ne(oe,{text:"Severity",children:(Ce,Ie)=>{lt(Ce,{class:"w-[90px]",get options(){return q},get value(){return e(l)},onchange:V})}});var ve=$(oe,2);Ne(ve,{text:"Search",children:(Ce,Ie)=>{var Ge=_d();Xe(Ge),le("input",Ge,C),et(Ge,()=>e(d),gt=>f(d,gt)),u(Ce,Ge)}});var ye=$(ve,2);Ne(ye,{text:"Context",children:(Ce,Ie)=>{var Ge=bd();Xe(Ge),le("change",Ge,G),et(Ge,()=>e(b),gt=>f(b,gt)),u(Ce,Ge)}});var ge=$(ye,2),_e=k(ge);{let Ce=j(()=>e(h).length===0);$e(_e,{variant:"secondary",size:"sm",onclick:fe,get disabled(){return e(Ce)},children:(Ie,Ge)=>{var gt=xd(),jt=K(gt);Te(jt,{get icon(){return pr}}),ke(),u(Ie,gt)},$$slots:{default:!0}})}p(ge),u(Le,Y)}});var re=$(ce,2);{var xe=Le=>{var Pe=Cd(),Y=$(k(Pe),2);Ye(Y,16,()=>[{key:"severity",label:"Severity"},{key:"count",label:"Count"},{key:"case_id",label:"Case"},{key:"file",label:"File"}],U=>U.key,(U,ee)=>{var oe=kd(),ve=k(oe,!0),ye=$(ve);{var ge=_e=>{var Ce=wd(),Ie=$(K(Ce));{let Ge=j(()=>e(D)==="asc"?Do:To);Te(Ie,{get icon(){return e(Ge)},size:12})}u(_e,Ce)};Q(ye,_e=>{e(H)===ee.key&&_e(ge)})}p(oe),J(()=>{Se(oe,1,`cursor-pointer bg-transparent border-none text-xs hover:underline ${e(H)===ee.key?"text-edf-bleu-fonce font-bold":"text-muted"}`),ne(ve,ee.label)}),le("click",oe,()=>ue(ee.key)),u(U,oe)}),p(Pe),u(Le,Pe)};Q(re,Le=>{e(B).length>0&&Le(xe)})}var he=$(re,2),Ee=k(he);{var pe=Le=>{var Pe=Sd();u(Le,Pe)};Q(Ee,Le=>{e(B).length===0&&Le(pe)})}var Fe=$(Ee,2);Ye(Fe,17,()=>e(B),Le=>Le.fingerprint,(Le,Pe)=>{var Y=Rd(),U=k(Y),ee=k(U),oe=k(ee);{let Je=j(()=>e(E).has(e(Pe).fingerprint)?pn:zo);Te(oe,{get icon(){return e(Je)},size:14})}p(ee);var ve=$(ee,2),ye=k(ve);p(ve);var ge=$(ve,2),_e=k(ge);{var Ce=Je=>{var ut=$d(),at=k(ut);p(ut),J(()=>ne(at,`${e(Pe).count??""}x`)),u(Je,ut)};Q(_e,Je=>{e(Pe).count>1&&Je(Ce)})}var Ie=$(_e,2);{var Ge=Je=>{var ut=Pd();u(Je,ut)};Q(Ie,Je=>{e(Pe).isNew&&Je(Ge)})}p(ge),p(U);var gt=$(U,2);{var jt=Je=>{var ut=Ed();zr(ut,()=>e(Pe).line_html,!0),p(ut),u(Je,ut)},jr=j(()=>e(E).has(e(Pe).fingerprint));Q(gt,Je=>{e(jr)&&Je(jt)})}p(Y),J(Je=>{Se(Y,1,`error-item ${Je??""}`),ne(ye,`${e(Pe).case_id??""} / ${e(Pe).file??""} : ${e(Pe).severity??""}`)},[()=>_(e(Pe).severity)]),le("click",U,()=>R(e(Pe).fingerprint)),u(Le,Y)}),p(he);var ze=$(he,2);{var Qe=Le=>{var Pe=Md(),Y=k(Pe),U=k(Y);p(Y);var ee=$(Y,2);{var oe=ye=>{var ge=Nd(),_e=k(ge);p(ge),J(Ce=>ne(_e,`${Ce??""} new`),[()=>e(h).filter(Ce=>Ce.isNew).length]),u(ye,ge)},ve=j(()=>e(h).some(ye=>ye.isNew));Q(ee,ye=>{e(ve)&&ye(oe)})}p(Pe),J(()=>ne(U,`${e(h).length??""} unique / ${e(N).length??""} total`)),u(Le,Pe)};Q(ze,Le=>{e(B).length>0&&Le(Qe)})}var je=$(ze,2);{var rt=Le=>{var Pe=Ad(),Y=k(Pe,!0);p(Pe),J(()=>ne(Y,e(A))),u(Le,Pe)};Q(je,Le=>{e(A)&&Le(rt)})}J(()=>Se(he,1,`error-list ${e(x)?"opacity-50 pointer-events-none":""}`)),u(te,de)},$$slots:{actions:!0,default:!0}}),Ae()}Ke(["input","change","click"]);var Fd=S('
        Developed by Simvia
        ',1);function Bd(t,r){Me(r,!0);let a=z(ie({totalCases:0,shownCases:0,totalRunning:0,shownRunning:0,totalConverged:0,shownConverged:0})),n=z(ie([])),s=z(null);const i=v=>e(s)===null||e(s).includes(v);async function o(v){var ue;const B=`/favicon-${v}.svg`;try{const fe=await fetch(B);if(!fe.ok||!((ue=fe.headers.get("content-type"))!=null&&ue.includes("svg")))return}catch{return}const X=document.querySelector('link[rel="icon"]');X&&(X.href=B)}async function l(){try{const v=await si();Rl(v.rows),Nl(v.doe_columns),f(n,v.rows.map(B=>B.case_id),!0),f(a,Ul($a(),yn()),!0)}catch(v){console.error("Failed to load status:",v)}}Zn(()=>{l(),oi().then(v=>{rl(v),f(s,v.panels,!0),o(v.solver)}).catch(()=>{})});var d=Fd(),b=K(d);Mi(b,{});var x=$(b,2);Di(x,{});var A=$(x,2);ll(A,{get totalCases(){return e(a).totalCases},get shownCases(){return e(a).shownCases},get totalRunning(){return e(a).totalRunning},get shownRunning(){return e(a).shownRunning},get totalConverged(){return e(a).totalConverged},get shownConverged(){return e(a).shownConverged}});var m=$(A,2),N=k(m);{var L=v=>{xc(v,{onRefresh:l})},g=j(()=>i("status"));Q(N,v=>{e(g)&&v(L)})}var c=$(N,2);{var w=v=>{Vc(v,{get allCases(){return e(n)}})},P=j(()=>i("residuals"));Q(c,v=>{e(P)&&v(w)})}var h=$(c,2);{var y=v=>{fu(v,{get allCases(){return e(n)}})},C=j(()=>i("probes"));Q(h,v=>{e(C)&&v(y)})}var _=$(h,2);{var M=v=>{ku(v,{get allCases(){return e(n)}})},F=j(()=>i("performance"));Q(_,v=>{e(F)&&v(M)})}var T=$(_,2);{var q=v=>{hd(v,{get allCases(){return e(n)}})},I=j(()=>i("compare"));Q(T,v=>{e(I)&&v(q)})}var O=$(T,2);{var V=v=>{Gu(v,{get allCases(){return e(n)}})},G=j(()=>i("tail"));Q(O,v=>{e(G)&&v(V)})}var Z=$(O,2);{var ae=v=>{Td(v,{get allCases(){return e(n)}})},E=j(()=>i("errors"));Q(Z,v=>{e(E)&&v(ae)})}p(m);var R=$(m,2),H=$(k(R),2),D=k(H);p(H),p(R),J(()=>we(D,"src",cl)),u(t,d),Ae()}export{Bd as component}; diff --git a/frontend/dist/_app/version.json b/frontend/dist/_app/version.json index 5535285..352b7e3 100644 --- a/frontend/dist/_app/version.json +++ b/frontend/dist/_app/version.json @@ -1 +1 @@ -{"version":"1789130391568"} \ No newline at end of file +{"version":"1791465900433"} \ No newline at end of file diff --git a/frontend/dist/code-saturne.svg b/frontend/dist/code-saturne.svg deleted file mode 100644 index 25da861..0000000 --- a/frontend/dist/code-saturne.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index c32cd83..46e7c5e 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -5,30 +5,30 @@ csauto by Simvia - - - - - - - - - + + + + + + + + + @@ -39,13 +38,16 @@ style="padding-inline: max(16px, calc((100vw - 1200px) / 2));" >
        - {#if !getAppConfig() || getAppConfig()?.solver === "code_saturne"} - CODE_SATURNE - {:else if getAppConfig()?.solver === "code_aster"} - CODE_ASTER - {:else} + + {#if config?.logo} + {config.solver} + {:else if config} {getAppConfig()?.solver}{config.solver} {/if}
        diff --git a/frontend/src/lib/components/performance/PerformanceCard.svelte b/frontend/src/lib/components/performance/PerformanceCard.svelte index c30b4bc..b5c29c1 100644 --- a/frontend/src/lib/components/performance/PerformanceCard.svelte +++ b/frontend/src/lib/components/performance/PerformanceCard.svelte @@ -1,7 +1,7 @@ {#if visible} - - {#snippet tabs()} -
        -
        Diagnostics
        -
        -
        +
        + - + -
        + ? 'text-edf-gris-moyen cursor-not-allowed opacity-50' + : activeTab === 'profile' + ? 'text-edf-bleu-fonce cursor-pointer' + : 'text-edf-gris-moyen cursor-pointer hover:text-muted'}" + onclick={() => switchTab("profile")} + disabled={!profileHasData} + title={!profileHasData + ? "No profile data available. Run a simulation first." + : ""} + > + Profiles +
        - {/snippet} - + + {/snippet} + {#snippet actions()} { const axis = state.axis || chooseAxis(cols); const values = cols @@ -93,8 +88,8 @@ state.files.map((f) => ({ value: f, label: f + .replace(/^.*\//, "") .replace(/\.csv$/i, "") - .replace(/^profiles\//i, "") .replace(/_/g, " "), })), ); @@ -120,13 +115,18 @@ let emptyMessage = $derived( state.selectedCases.length === 0 ? "Please select at least one case." - : state.selectedColumns.length === 0 - ? "Please select at least one value." - : "No data to display.", + : state.files.length === 0 + ? "No profile data for the selected cases yet." + : state.selectedColumns.length === 0 + ? "Please select at least one value." + : "No data to display.", ); -{#if state.files.length > 0} + +{#if allCases.length > 0} files.filter((f) => { const stem = f.replace(/\.csv$/i, "").toLowerCase(); @@ -132,13 +127,18 @@ let emptyMessage = $derived( state.selectedCases.length === 0 ? "Please select at least one case." - : state.selectedColumns.length === 0 - ? "Please select at least one probe." - : "No data to display.", + : state.files.length === 0 + ? "No probe data for the selected cases yet." + : state.selectedColumns.length === 0 + ? "Please select at least one probe." + : "No data to display.", ); -{#if state.files.length > 0} + +{#if allCases.length > 0} , so Tab alone + would never reach it); Escape or a selection puts focus back on the button. --> - + - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/static/code-saturne.svg b/frontend/static/code-saturne.svg deleted file mode 100644 index 25da861..0000000 --- a/frontend/static/code-saturne.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/static/favicon-code_aster.svg b/frontend/static/favicon-code_aster.svg deleted file mode 100644 index baf0ec0..0000000 --- a/frontend/static/favicon-code_aster.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/static/favicon-code_saturne.svg b/frontend/static/favicon-code_saturne.svg deleted file mode 100644 index dc6e4fa..0000000 --- a/frontend/static/favicon-code_saturne.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/README.md b/tests/README.md index e971693..0c77a8f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,6 +24,27 @@ pytest tests/web -q - `tests/runner/` - launch/status/kill orchestration behavior - `tests/web/` - HTTP API behavior +## Solver adapters + +- `tests/unit/test_adapter_conformance.py` runs the same contract checks on + every registered solver adapter, against a sample finished case written by a + `sample_` function. A new adapter adds its own sample there (see + [Adding a new solver](../docs/adding-a-solver.md)). +- `tests/unit/test_solver_boundary.py` scans every module outside + `csauto/solvers/` for code_saturne file names and log markers. +- `tests/integration/test_docker_solvers.py` runs the shipped code_saturne and + code_aster examples for real in their docker images. It is opt-in: + +```bash +docker pull simvia/code_saturne +docker pull simvia/code_aster:17.4.0 +CSAUTO_DOCKER_TESTS=1 pytest tests/integration/test_docker_solvers.py -q +``` + +These runs write their cases under `CSAUTO_DOCKER_TEST_DIR` (default +`~/.cache/csauto-docker-tests`) instead of `tmp_path`, because Docker Desktop on +WSL2 cannot mount `/tmp`. + ## Testing guidelines - test public APIs and public behavior @@ -36,7 +57,8 @@ pytest tests/web -q Common fixtures are defined in `tests/conftest.py`: - `runs_dir` -- `case_factory` +- `case_factory`: writes a case with `DATA/setup.xml` and the `doe_row.csv` + by which csauto recognizes a case folder - `registry_factory` ## When adding tests diff --git a/tests/conftest.py b/tests/conftest.py index 880f7c4..26a8153 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,13 @@ os.environ["CSAUTO_TELEMETRY_DISABLED"] = "1" +@pytest.fixture(autouse=True) +def _no_docker_image_probe(request, monkeypatch) -> None: + """Unit tests fake docker, so the image entrypoint lookup must not call the real one.""" + if "test_docker_solvers" not in request.node.nodeid and "test_image_entrypoint" not in request.node.name: + monkeypatch.setattr("csauto.docker.image_entrypoint", lambda image, solver_bin: None) + + @pytest.fixture() def runs_dir(tmp_path: Path) -> Path: runs_dir = tmp_path / "RUNS" @@ -24,6 +31,8 @@ def make_case(runs_dir: Path, case_id: str = "case0001") -> Path: case_dir = runs_dir / case_id (case_dir / "DATA").mkdir(parents=True, exist_ok=True) (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") + # csauto prepare writes one per case; it is how a folder is known to be a case. + (case_dir / "doe_row.csv").write_text(f"case_id\n{case_id}\n", encoding="utf-8") return case_dir diff --git a/tests/integration/test_campaign_solver.py b/tests/integration/test_campaign_solver.py new file mode 100644 index 0000000..2a7d698 --- /dev/null +++ b/tests/integration/test_campaign_solver.py @@ -0,0 +1,66 @@ +"""A campaign remembers the solver it was prepared for, whatever directory commands run from.""" + +from __future__ import annotations + +from pathlib import Path + +from csauto.cli import main +from csauto.registry import read_campaign_solver + + +def _stub_study(root: Path) -> None: + (root / "TEMPLATE").mkdir(parents=True) + (root / "TEMPLATE" / "stub.toml").write_text("steps = {steps}\n", encoding="utf-8") + (root / "doe.csv").write_text("steps\n2\n3\n", encoding="utf-8") + (root / "csauto.toml").write_text('solver = "stub"\n', encoding="utf-8") + + +def test_prepare_records_the_solver_and_later_commands_use_it(tmp_path: Path, monkeypatch, capsys) -> None: + study = tmp_path / "study" + _stub_study(study) + monkeypatch.chdir(study) + assert main(["prepare", "doe.csv", "TEMPLATE", "RUNS"]) == 0 + assert read_campaign_solver(study / "RUNS") == "stub" + + # From another directory, with no csauto.toml around, doctor still checks stub cases. + elsewhere = tmp_path / "elsewhere" + elsewhere.mkdir() + monkeypatch.chdir(elsewhere) + monkeypatch.delenv("CSAUTO_CONFIG", raising=False) + capsys.readouterr() + assert main(["doctor", str(study / "RUNS")]) == 0 + output = capsys.readouterr().out + assert "solver stub:" in output + assert "2 cases detected" in output + + +def test_prepare_refuses_to_mix_solvers_in_one_campaign(tmp_path: Path, monkeypatch, capsys) -> None: + study = tmp_path / "study" + _stub_study(study) + monkeypatch.chdir(study) + assert main(["prepare", "doe.csv", "TEMPLATE", "RUNS"]) == 0 + (study / "csauto.toml").write_text('solver = "code_aster"\n', encoding="utf-8") + assert main(["prepare", "doe.csv", "TEMPLATE", "RUNS"]) == 1 + assert "holds a stub campaign" in capsys.readouterr().err + + +def test_a_campaign_of_another_solver_drops_that_solvers_settings(tmp_path: Path, capsys) -> None: + from csauto.cli import _use_campaign_solver + from csauto.config import Config + from csauto.registry import write_campaign_solver + + runs = tmp_path / "RUNS" + write_campaign_solver(runs, "stub") + config = Config(solver="code_saturne", docker_image="my/saturne", saturne_bin="/opt/cs", path=tmp_path / "a.toml") + assert _use_campaign_solver(runs, config) + assert (config.solver, config.docker_image, config.saturne_bin) == ("stub", None, None) + assert not _use_campaign_solver(runs, config) # already on the recorded solver + + +def test_an_unknown_recorded_solver_is_a_clear_error(tmp_path: Path, monkeypatch, capsys) -> None: + runs = tmp_path / "RUNS" + runs.mkdir() + (runs / "campaign.json").write_text('{"solver": "nope"}', encoding="utf-8") + monkeypatch.chdir(tmp_path) + assert main(["status", str(runs)]) == 1 + assert "campaign.json names an unknown solver: 'nope'" in capsys.readouterr().err diff --git a/tests/integration/test_code_aster_adapter.py b/tests/integration/test_code_aster_adapter.py deleted file mode 100644 index 1efc1ef..0000000 --- a/tests/integration/test_code_aster_adapter.py +++ /dev/null @@ -1,126 +0,0 @@ -"""End-to-end tests driving the pipeline with the code_aster solver adapter. - -The launch test drives a real `docker run` of the code_aster image, so it -only runs when CSAUTO_CODE_ASTER_DOCKER_TESTS=1 is set (docker and the -image must be available); the generate/doctor/cleanup test is -filesystem-only and always runs. -""" - -from __future__ import annotations - -import os -import time -from pathlib import Path - -import pytest - -from csauto.registry import STATUS_DONE, STATUS_RUNNING, load_registry -from csauto.runner import refresh_status, run_cases -from csauto.solvers import get_solver_adapter - - -def _make_code_aster_case(runs_dir: Path, case_id: str) -> Path: - case_dir = runs_dir / case_id - case_dir.mkdir(parents=True) - mesh_dir = case_dir / "MESH" - mesh_dir.mkdir(parents=True) - (case_dir / "study.export").write_text( - "P time_limit 300\nP memory_limit 1024\nF comm study.comm D 1\nF mmed MESH/mesh.med D 20\n", encoding="utf-8" - ) - (case_dir / "study.comm").write_text("DEBUT()\nFIN()\n", encoding="utf-8") - (mesh_dir / "mesh.med").write_text("x\n", encoding="utf-8") - return case_dir - - -def _wait_for(predicate, timeout: float = 10.0) -> bool: - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if predicate(): - return True - time.sleep(0.05) - return False - - -@pytest.mark.skipif( - os.environ.get("CSAUTO_CODE_ASTER_DOCKER_TESTS") != "1", - reason="set CSAUTO_CODE_ASTER_DOCKER_TESTS=1 with docker and the simvia/code_aster image pulled", -) -def test_run_cases_launches_code_aster_solver(runs_dir: Path) -> None: - adapter = get_solver_adapter("code_aster") - case_dir = _make_code_aster_case(runs_dir, "case0001") - - run_cases( - runs_dir, - nprocs=1, - nt=1, - max_parallel=1, - case_filter=["case0001"], - runtime="docker", - resume_only_failed=False, - source="test", - adapter=adapter, - ) - - registry = load_registry(runs_dir) - record = registry["case0001"] - assert record["status"] == STATUS_RUNNING - assert record["runtime"] == "docker" - assert record["pid"] - - codeaster_log = case_dir / "RESU" / "LOGS" / "run_solver.log" - assert _wait_for(codeaster_log.is_file, timeout=120.0), "code_aster solver never wrote its log" - assert _wait_for(lambda: "DIAGNOSTIC JOB" in codeaster_log.read_text(encoding="utf-8"), timeout=120.0) - assert "code_aster" in codeaster_log.read_text(encoding="utf-8") - assert adapter.detect_outcome(case_dir) == STATUS_DONE - assert (case_dir / "RESU").exists() - - def _status_done() -> bool: - rows = refresh_status(runs_dir, adapter=adapter) - return bool(rows) and rows[0]["status"] == STATUS_DONE - - assert _wait_for(_status_done), "refresh_status never reported the code_aster run as DONE" - registry = load_registry(runs_dir) - assert registry["case0001"]["status"] == STATUS_DONE - - -def test_generate_doctor_and_cleanup_with_code_aster_solver(tmp_path: Path) -> None: - from csauto.doe import generate_cases - from csauto.maintenance import cleanup_runs, run_doctor - - adapter = get_solver_adapter("code_aster") - template_dir = tmp_path / "template" - template_dir.mkdir() - mesh_dir = tmp_path / "MESH" - mesh_dir.mkdir(parents=True) - resu_dir = tmp_path / "RESU" - resu_dir.mkdir(parents=True) - (template_dir / "study.export").write_text( - "P time_limit 300\nP memory_limit 1024\nF comm study.comm D 1\nF mmed MESH/mesh.med D 20\n", encoding="utf-8" - ) - (template_dir / "study.comm").write_text("DEBUT()\nE = {young}\nFIN()\n", encoding="utf-8") - (mesh_dir / "mesh.med").write_text("x\n", encoding="utf-8") - output_dir = tmp_path / "RUNS" - - generate_cases( - ["case_id", "young"], - [{"case_id": "case0001", "young": "1.5"}, {"case_id": "case0002", "young": "2.5"}], - template_dir, - output_dir, - adapter=adapter, - ) - - assert "E = 1.5" in (output_dir / "case0001" / "study.comm").read_text(encoding="utf-8") - assert "E = 2.5" in (output_dir / "case0002" / "study.comm").read_text(encoding="utf-8") - assert (output_dir / "MESH").exists() - - items = run_doctor(output_dir, check_display=False, adapter=adapter) - messages = [item.message for item in items] - assert "solver setup file present in every case" in messages - assert not any(item.level == "fail" for item in items) - - run_dir = output_dir / "case0001" / "RESU" / "LOGS" - run_dir.mkdir(parents=True) - (run_dir / "run_solver.log").write_text("code_aster\nDIAGNOSTIC JOB : OK\n", encoding="utf-8") - report = cleanup_runs(output_dir, prune_resu=True, keep_last=0, adapter=adapter) - assert report.resu_removed == 1 - assert not run_dir.exists() diff --git a/tests/integration/test_docker_solvers.py b/tests/integration/test_docker_solvers.py new file mode 100644 index 0000000..c2658e7 --- /dev/null +++ b/tests/integration/test_docker_solvers.py @@ -0,0 +1,134 @@ +"""Real runs of the shipped examples in the solvers' docker images. + +Opt-in: set CSAUTO_DOCKER_TESTS=1 with docker running and the images pulled +(simvia/code_saturne, simvia/code_aster:17.4.0). Cases are written under +CSAUTO_DOCKER_TEST_DIR (default ~/.cache/csauto-docker-tests), because Docker +Desktop cannot mount /tmp from WSL2. The filesystem-only test always runs. +""" + +from __future__ import annotations + +import os +import shutil +import time +import uuid +from pathlib import Path + +import pytest + +from csauto.cli import main +from csauto.logs import list_tail_files +from csauto.registry import STATUS_DONE, STATUS_FAILED, STATUS_RUNNING +from csauto.runner import refresh_status +from csauto.solvers import get_solver_adapter + +EXAMPLES = Path(__file__).resolve().parents[2] / "examples" +needs_docker = pytest.mark.skipif( + os.environ.get("CSAUTO_DOCKER_TESTS") != "1", + reason="set CSAUTO_DOCKER_TESTS=1 with docker and the solver images pulled", +) + + +@pytest.fixture() +def study_dir(): + base = Path(os.environ.get("CSAUTO_DOCKER_TEST_DIR", Path.home() / ".cache" / "csauto-docker-tests")) + path = base / uuid.uuid4().hex[:8] + path.mkdir(parents=True) + yield path + shutil.rmtree(path, ignore_errors=True) + + +def _copy_example(name: str, dest: Path, rows: int) -> None: + for entry in (EXAMPLES / name).iterdir(): + if entry.name in {"RUNS", "README.md"}: + continue + target = dest / entry.name + shutil.copytree(entry, target) if entry.is_dir() else shutil.copy(entry, target) + lines = (dest / "doe.csv").read_text(encoding="utf-8").splitlines() + (dest / "doe.csv").write_text("\n".join(lines[: rows + 1]) + "\n", encoding="utf-8") + + +def _run_campaign(study: Path, solver: str, monkeypatch, timeout: float = 600.0) -> dict[str, dict]: + monkeypatch.chdir(study) + assert main(["prepare", "doe.csv", "TEMPLATE", "RUNS"]) == 0 + assert main(["run", "RUNS", "--n", "1", "--nt", "1", "--max-parallel", "2"]) == 0 + adapter = get_solver_adapter(solver) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + rows = {row["case_id"]: row for row in refresh_status(study / "RUNS", adapter=adapter)} + if not any(row["status"] == STATUS_RUNNING for row in rows.values()): + return rows + time.sleep(3) + pytest.fail(f"{solver} runs did not finish in {timeout} s") + + +@needs_docker +def test_code_saturne_example_runs_in_docker(study_dir: Path, monkeypatch) -> None: + _copy_example("single-case", study_dir, rows=2) + setup = study_dir / "TEMPLATE" / "DATA" / "setup.xml" + setup.write_text( + setup.read_text(encoding="utf-8").replace("500", "20") + ) + + rows = _run_campaign(study_dir, "code_saturne", monkeypatch) + + assert {row["status"] for row in rows.values()} == {STATUS_DONE} + assert {row["last_iter"] for row in rows.values()} == {20} + adapter = get_solver_adapter("code_saturne") + case_dir = study_dir / "RUNS" / "case0001" + assert list_tail_files(case_dir, adapter)[0] == "run_solver.log" + assert adapter.find_residuals_files(case_dir) and adapter.list_probe_files(case_dir) + assert not any(p.name.startswith(".") for p in (study_dir / "RUNS").iterdir() if p.is_dir()) + + +@needs_docker +def test_code_aster_example_runs_in_docker(study_dir: Path, monkeypatch) -> None: + # Row 1 imposes a displacement the command file rejects on purpose; row 2 is valid. + _copy_example("codeaster-cube", study_dir, rows=2) + + rows = _run_campaign(study_dir, "code_aster", monkeypatch) + + assert rows["case0001"]["status"] == STATUS_FAILED + assert rows["case0002"]["status"] == STATUS_DONE + case_dir = study_dir / "RUNS" / "case0002" + assert (case_dir / "RESU" / "myresults.rmed").is_file() + assert (case_dir / ".csauto.export").is_file() # the export actually run, with n and nt + assert list_tail_files(case_dir, get_solver_adapter("code_aster"))[0] == "csauto.stdout" + # Preparing again after a launch must still recognise every case. + assert main(["prepare", "doe.csv", "TEMPLATE", "RUNS"]) == 0 + + +def test_generate_doctor_and_cleanup_with_code_aster_solver(tmp_path: Path) -> None: + from csauto.doe import generate_cases + from csauto.maintenance import cleanup_runs, run_doctor + + adapter = get_solver_adapter("code_aster") + template_dir = tmp_path / "TEMPLATE" + template_dir.mkdir() + (tmp_path / "MESH").mkdir() + (tmp_path / "MESH" / "mesh.med").write_text("x\n", encoding="utf-8") + (template_dir / "study.export").write_text( + "P time_limit 300\nF comm study.comm D 1\nF mmed ../MESH/mesh.med D 20\n", encoding="utf-8" + ) + (template_dir / "study.comm").write_text("DEBUT()\nE = {young}\nFIN()\n", encoding="utf-8") + output_dir = tmp_path / "RUNS" + + generate_cases( + ["case_id", "young"], + [{"case_id": "case0001", "young": "1.5"}, {"case_id": "case0002", "young": "2.5"}], + template_dir, + output_dir, + adapter=adapter, + ) + + assert "E = 1.5" in (output_dir / "case0001" / "study.comm").read_text(encoding="utf-8") + assert (output_dir / "MESH").is_symlink() + items = run_doctor(output_dir, check_display=False, adapter=adapter) + assert "solver setup file present in every case" in [item.message for item in items] + assert not any(item.level == "fail" for item in items) + + (output_dir / "case0001" / "RESU").mkdir() + (output_dir / "case0001" / "RESU" / "result.rmed").write_text("x", encoding="utf-8") + assert cleanup_runs(output_dir, prune_resu=True, keep_last=1, adapter=adapter).resu_removed == 0 + assert cleanup_runs(output_dir, prune_resu=True, keep_last=0, adapter=adapter).resu_removed == 1 + assert not (output_dir / "case0001" / "RESU").exists() diff --git a/tests/integration/test_recent_errors.py b/tests/integration/test_recent_errors.py index e209ea8..95d991c 100644 --- a/tests/integration/test_recent_errors.py +++ b/tests/integration/test_recent_errors.py @@ -142,3 +142,18 @@ def open_wrapper(self: Path, *args, **kwargs): assert seek_offsets[0] == 0 assert seek_offsets[-1] == initial_size + + +def test_run_asters_mpi_notice_is_not_an_error(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + case_dir = tmp_path / "RUNS" / "case0001" + case_dir.mkdir(parents=True) + notice = "WARNING: If MPI_Abort is called during execution, result files could not be copied.\n" + (case_dir / "csauto.stdout").write_text(notice + "! real failure\n", encoding="utf-8") + + items = collect_recent_errors( + tmp_path / "RUNS", ["case0001"], files=["csauto.stdout"], adapter=get_solver_adapter("code_aster") + ) + assert [item["severity"] for item in items] == ["error"] + assert "CALCULEL_8" in items[0]["line_html"] diff --git a/tests/integration/test_render_svg.py b/tests/integration/test_render_svg.py index 7764720..31bf4da 100644 --- a/tests/integration/test_render_svg.py +++ b/tests/integration/test_render_svg.py @@ -147,7 +147,7 @@ def test_render_residuals_svg_warns_for_launched_case_missing_residuals(tmp_path read_residual_rows(runs_dir, ["case0001"], allow_empty=True) - assert "residuals.csv not found for case0001" in capsys.readouterr().err + assert "residuals not found for case0001" in capsys.readouterr().err def test_read_probe_rows_no_warning_for_prepared_case(tmp_path: Path, capsys) -> None: @@ -178,3 +178,73 @@ def test_read_probe_rows_warns_for_launched_case_missing_probe(tmp_path: Path, c read_probe_rows(runs_dir, ["case0001"], "probe_density.csv", allow_empty=True) assert "probe not found for case0001" in capsys.readouterr().err + + +def test_probe_rendering_follows_an_adapter_declared_layout(tmp_path: Path) -> None: + """A solver with its own probe directory reuses CSV reading and SVG rendering as is.""" + from csauto.probes import list_run_csv_files, locate_run_csv_files + from csauto.solvers.stub import StubAdapter + + class ProbingStub(StubAdapter): + def list_probe_files(self, case_dir, limit=200): + return list_run_csv_files(self.latest_run_dir(case_dir), "points", limit=limit) + + def locate_probe_files(self, case_dir, probe_ref, include_history=False): + return locate_run_csv_files(case_dir, self.list_run_dirs(case_dir), ("points",), probe_ref, include_history) + + runs_dir = tmp_path / "RUNS" + probe_dir = runs_dir / "case0001" / "OUT" / "run_0001" / "points" + probe_dir.mkdir(parents=True) + (probe_dir / "probe_density.csv").write_text("time,value\n0.0,1.0\n1.0,2.0\n", encoding="utf-8") + adapter = ProbingStub() + + assert adapter.list_probe_files(runs_dir / "case0001") == ["probe_density.csv"] + _header, rows = read_probe_rows(runs_dir, ["case0001"], "probe_density.csv", adapter=adapter) + assert [row["value"] for row in rows] == ["1.0", "2.0"] + svg = render_probe_svg(runs_dir, ["case0001"], "probe_density.csv", ["value"], adapter=adapter) + assert " None: + from csauto.solvers.stub import StubAdapter + + class LogResiduals(StubAdapter): + def parse_live_residuals(self, case_dir): + return ["r"], [{"r": "1e-1"}, {"r": "1e-2"}, {"r": "1e-3"}] + + runs_dir = tmp_path / "RUNS" + (runs_dir / "case0001").mkdir(parents=True) + svg = render_residuals_svg(runs_dir, ["case0001"], ["r"], adapter=LogResiduals()) + assert " None: + runs_dir = tmp_path / "RUNS" + probe_dir = runs_dir / "case0001" / "RESU" / "001" / "monitoring" + probe_dir.mkdir(parents=True) + (probe_dir / "probe_h.csv").write_text("Time,value\n0.0,1.0\n1.0,2.0\n", encoding="utf-8") + svg = render_probe_svg(runs_dir, ["case0001"], "probe_h.csv", ["value"], axis="time") + assert " None: + from csauto.probes import probe_position + + case_dir = tmp_path / "RUNS" / "case0001" + probe_dir = case_dir / "RESU" / "001" / "monitoring" + probe_dir.mkdir(parents=True) + (probe_dir / "gauges_coords.csv").write_text("name,x,y\ngauge_A,1,1\n", encoding="utf-8") + assert probe_position(case_dir, "gauges_h.csv", column_ref="gauge_A")["x"] == 1.0 + assert probe_position(case_dir, "gauges_h.csv", column_ref="gauge_B") == {} + + +def test_read_csv_table_can_stop_early(tmp_path: Path) -> None: + from csauto.probes import read_csv_table + + path = tmp_path / "probes.csv" + path.write_text("t, p1\n" + "".join(f"{i}, {i * 2}\n" for i in range(500)), encoding="utf-8") + assert read_csv_table(path, max_rows=0) == (["t", "p1"], []) + rows = read_csv_table(path, max_rows=200)[1] + assert len(rows) == 200 and rows[-1] == {"t": "199", "p1": "398"} + assert len(read_csv_table(path)[1]) == 500 diff --git a/tests/runner/test_runner.py b/tests/runner/test_runner.py index 686e0db..7b63c11 100644 --- a/tests/runner/test_runner.py +++ b/tests/runner/test_runner.py @@ -11,6 +11,7 @@ STATUS_DONE, STATUS_FAILED, STATUS_PENDING, + STATUS_PREPARED, STATUS_RUNNING, load_registry, registry_transaction, @@ -113,7 +114,15 @@ def popen_stub(args, *_fargs, **_kwargs): assert record["pid"] == 23456 assert record["runtime"] == "native" assert popen_calls - assert popen_calls[0][:3] == ["nohup", str(saturne_bin.resolve()), "run"] + # The command runs behind a shell that records its exit status. + assert popen_calls[0][:5] == [ + "nohup", + "sh", + "-c", + '"$@"; rc=$?; echo $rc > .csauto.exitcode; exit $rc', + "csauto-run", + ] + assert popen_calls[0][5:7] == [str(saturne_bin.resolve()), "run"] def test_run_cases_restart_adds_restart_args(monkeypatch, runs_dir: Path, case_factory) -> None: @@ -216,6 +225,8 @@ def popen_fail(*_args, **_kwargs): wrap_value = submit_calls[0][submit_calls[0].index("--wrap") + 1] assert "CS_MPIEXEC_OPTIONS=" in wrap_value assert "--bind-to core" in wrap_value + # The job ends with the solver's status, so Slurm sees failures too. + assert wrap_value.endswith("; rc=$?; echo $rc > .csauto.exitcode; exit $rc") def test_run_cases_docker_runtime_can_submit_via_slurm(monkeypatch, runs_dir: Path, case_factory) -> None: @@ -331,7 +342,7 @@ def popen_fail(*_args, **_kwargs): assert "--cpus-per-task" in submit_calls[0] assert submit_calls[0][submit_calls[0].index("--cpus-per-task") + 1] == "1" script_path = Path(submit_calls[0][-1]) - assert script_path == case_dir / ".csauto.slurm.singularity.sh" + assert script_path == case_dir / ".csauto.slurm.sh" script_text = script_path.read_text(encoding="utf-8") assert "=== Step 1/3: Case preparation ===" in script_text assert "=== Step 2/3: Solver execution ===" in script_text @@ -444,7 +455,7 @@ def run_target(case_id: str) -> None: monkeypatch.setattr("csauto.solvers.code_saturne.CodeSaturneAdapter.build_restart_args", restart_stub) monkeypatch.setattr("subprocess.Popen", popen_stub) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: True) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: True) monkeypatch.setattr("csauto.runner.time.sleep", sleep_stub) first = threading.Thread(target=run_target, args=("case0001",)) @@ -501,7 +512,7 @@ def test_refresh_status_reads_last_iter(monkeypatch, runs_dir: Path, case_factor registry["case0001"]["start_time"] = "2020-01-01T00:00:00" save_registry(runs_dir, registry) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: True) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: True) rows = refresh_status(runs_dir) assert rows[0]["last_iter"] == 12 @@ -529,7 +540,7 @@ def sleep_stub(_seconds: float) -> None: raise StopSleep() monkeypatch.setattr("csauto.runner.read_container_id", lambda *_args, **_kwargs: None) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: True) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: True) monkeypatch.setattr("csauto.runner.time.sleep", sleep_stub) monkeypatch.setattr("shutil.which", lambda _name: "/bin/true") monkeypatch.setattr("subprocess.Popen", popen_stub) @@ -566,7 +577,7 @@ def test_refresh_status_marks_done_when_pid_dead(monkeypatch, runs_dir: Path, ca registry["case0001"]["start_time"] = (datetime.now() - timedelta(seconds=5)).isoformat(timespec="seconds") save_registry(runs_dir, registry) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) rows = refresh_status(runs_dir) assert rows[0]["status"] == STATUS_DONE @@ -575,6 +586,18 @@ def test_refresh_status_marks_done_when_pid_dead(monkeypatch, runs_dir: Path, ca assert registry["case0001"]["end_time"] +def test_refresh_status_leaves_a_queued_case_queued(runs_dir: Path, case_factory, registry_factory) -> None: + case_dir = case_factory(runs_dir, "case0001") + (case_dir / "run_solver.log").write_text("END OF CALCULATION\n", encoding="utf-8") # the previous run's + registry_factory(runs_dir, "case0001", case_dir, status=STATUS_PENDING) + registry = load_registry(runs_dir) + registry["case0001"]["start_time"] = (datetime.now() - timedelta(hours=1)).isoformat(timespec="seconds") + save_registry(runs_dir, registry) + + assert refresh_status(runs_dir)[0]["status"] == STATUS_PENDING + assert load_registry(runs_dir)["case0001"]["status"] == STATUS_PENDING + + def test_refresh_status_skips_stale_merge_when_case_relaunched( monkeypatch, runs_dir: Path, case_factory, registry_factory ) -> None: @@ -596,8 +619,8 @@ def mutate_with_relaunch(runs_dir_arg: Path, mutator) -> bool: save_registry(runs_dir_arg, current) return changed - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) - monkeypatch.setattr("csauto.solvers.code_saturne.detect_run_outcome", lambda *_args, **_kwargs: STATUS_DONE) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) + monkeypatch.setattr("csauto.solvers.code_saturne.scan_outcome", lambda *_args, **_kwargs: STATUS_DONE) monkeypatch.setattr("csauto.runner.mutate_registry", mutate_with_relaunch) rows = refresh_status(runs_dir) @@ -664,7 +687,7 @@ class DummyProc: monkeypatch.setattr("subprocess.Popen", lambda *_a, **_k: DummyProc()) monkeypatch.setattr("csauto.runner.read_container_id", lambda *_a, **_k: None) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) monkeypatch.setattr("csauto.runner._is_slurm_job_active", lambda _jid: False) monkeypatch.setattr("shutil.which", lambda _name: "/bin/true") @@ -699,7 +722,7 @@ def fake_killpg(pid, sig): signals_sent.append((pid, sig)) monkeypatch.setattr("os.killpg", fake_killpg) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) import signal @@ -718,7 +741,7 @@ def fake_killpg(pid, sig): signals_sent.append((pid, sig)) monkeypatch.setattr("os.killpg", fake_killpg) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: True) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: True) monkeypatch.setattr("csauto.runner.time.sleep", lambda _s: None) terminate_pid(42, grace=0.0) @@ -753,7 +776,7 @@ def fake_kill(pid, sig): monkeypatch.setattr("os.killpg", fake_killpg) monkeypatch.setattr("os.kill", fake_kill) - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) terminate_pid(42, grace=0.5) @@ -827,10 +850,14 @@ def test_build_restart_args_physical_time_missing_value(tmp_path: Path) -> None: # --------------------------------------------------------------------------- +def _make_checkpoint(run_dir: Path) -> None: + (run_dir / "checkpoint").mkdir(parents=True) + (run_dir / "checkpoint" / "main.csc").touch() + + def test_build_restart_args_explicit_path(tmp_path: Path) -> None: case_dir = tmp_path / "case0001" - resu_dir = case_dir / "RESU" / "my_run" - resu_dir.mkdir(parents=True) + _make_checkpoint(case_dir / "RESU" / "my_run") args, details = CS_ADAPTER.build_restart_args( case_dir, @@ -846,8 +873,7 @@ def test_build_restart_args_explicit_path(tmp_path: Path) -> None: def test_build_restart_args_explicit_resu_checkpoint_path(tmp_path: Path) -> None: case_dir = tmp_path / "case0001" - resu_dir = case_dir / "RESU" / "002" - resu_dir.mkdir(parents=True) + _make_checkpoint(case_dir / "RESU" / "002") args, details = CS_ADAPTER.build_restart_args( case_dir, @@ -860,6 +886,17 @@ def test_build_restart_args_explicit_resu_checkpoint_path(tmp_path: Path) -> Non assert any("--restart=002" in arg for arg in args) +def test_build_restart_args_run_without_checkpoint_raises(tmp_path: Path) -> None: + case_dir = tmp_path / "case0001" + (case_dir / "RESU" / "failed_run").mkdir(parents=True) + _make_checkpoint(case_dir / "RESU" / "good_run") + + with pytest.raises(ValueError, match="failed_run of case0001 has no checkpoint"): + CS_ADAPTER.build_restart_args(case_dir, restart_mode=None, restart_value=None, restart_path="failed_run") + _, details = CS_ADAPTER.build_restart_args(case_dir, restart_mode=None, restart_value=None, restart_path=None) + assert details["restart_run_id"] == "good_run" + + def test_build_restart_args_invalid_path_raises(tmp_path: Path) -> None: case_dir = tmp_path / "case0001" case_dir.mkdir(parents=True) @@ -1002,7 +1039,7 @@ def test_refresh_status_include_doe_returns_columns( ) registry_factory(runs_dir, "case0001", case_dir, status="PREPARED") - monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid: False) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) rows, doe_columns = refresh_status(runs_dir, include_doe=True) assert len(rows) == 1 @@ -1086,3 +1123,115 @@ def popen_stub(*_args, **_kwargs): ) assert load_registry(runs_dir)["case0001"]["status"] == STATUS_FAILED + + +def test_exit_status_decides_when_the_logs_give_no_verdict(runs_dir: Path, case_factory) -> None: + from csauto.runner import EXIT_CODE_FILE, final_outcome + from csauto.solvers import get_solver_adapter + + case_dir = case_factory(runs_dir, "case0001") + adapter = get_solver_adapter("code_saturne") + assert final_outcome(adapter, case_dir, None) == STATUS_FAILED # no verdict, no exit status + (case_dir / EXIT_CODE_FILE).write_text("0\n", encoding="utf-8") + assert final_outcome(adapter, case_dir, None) == STATUS_DONE + (case_dir / EXIT_CODE_FILE).write_text("3\n", encoding="utf-8") + assert final_outcome(adapter, case_dir, None) == STATUS_FAILED + # An exit status left by an earlier run does not count. + later = (datetime.now() + timedelta(minutes=5)).isoformat(timespec="seconds") + (case_dir / EXIT_CODE_FILE).write_text("0\n", encoding="utf-8") + assert final_outcome(adapter, case_dir, later) == STATUS_FAILED + + +def test_a_live_process_stays_running_even_after_its_log_verdict( + monkeypatch, runs_dir: Path, case_factory, registry_factory +) -> None: + case_dir = case_factory(runs_dir, "case0001") + (case_dir / "run_solver.log").write_text("END OF CALCULATION\n", encoding="utf-8") + start = (datetime.now() - timedelta(seconds=5)).isoformat(timespec="seconds") + with registry_transaction(runs_dir) as registry: + update_case(registry, "case0001", path=str(case_dir), status=STATUS_RUNNING, pid=4242, start_time=start) + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: True) + assert refresh_status(runs_dir)[0]["status"] == STATUS_RUNNING + monkeypatch.setattr("csauto.runner.is_process_alive", lambda _pid, **_kw: False) + assert refresh_status(runs_dir)[0]["status"] == STATUS_DONE + + +def test_a_reused_pid_counts_as_dead(monkeypatch) -> None: + import os + + from csauto import runner + + monkeypatch.setattr(runner, "_is_zombie", lambda _pid: False) + identity = runner.process_identity(os.getpid()) + assert identity and identity.count(":") >= 1 + assert runner.is_process_alive(os.getpid(), identity=identity) + assert not runner.is_process_alive(os.getpid(), identity="another-boot:123") + # No identity recorded (older records): only the PID counts. + assert runner.is_process_alive(os.getpid()) + + +def test_a_pid_owned_by_another_user_is_still_checked_by_identity(monkeypatch) -> None: + from csauto import runner + + def denied(_pid, _signal): + raise PermissionError + + monkeypatch.setattr(runner.os, "kill", denied) + monkeypatch.setattr(runner, "_is_zombie", lambda _pid: False) + monkeypatch.setattr(runner, "process_identity", lambda _pid: "boot:999") + assert runner.is_process_alive(4242, identity="boot:999") + assert not runner.is_process_alive(4242, identity="boot:1") + # procfs unreadable: the PID is all there is to go on. + monkeypatch.setattr(runner, "process_identity", lambda _pid: None) + assert runner.is_process_alive(4242, identity="boot:1") + + +def test_a_refused_launch_restores_the_case_and_continues_the_batch(monkeypatch, runs_dir: Path, case_factory) -> None: + for case_id in ("case0001", "case0002"): + case_factory(runs_dir, case_id) + with registry_transaction(runs_dir) as registry: + update_case(registry, "case0001", status=STATUS_DONE) + update_case(registry, "case0002", status=STATUS_PREPARED) + started: list[str] = [] + + def restart_stub(self, case_dir, *_args): + if case_dir.name == "case0001": + raise ValueError("No checkpoint found for case0001") + started.append(case_dir.name) + return [], {} + + monkeypatch.setattr("csauto.solvers.code_saturne.CodeSaturneAdapter.build_restart_args", restart_stub) + monkeypatch.setattr("csauto.runner._launch_local", lambda case_dir, case_id, *_a: started.append("launched")) + saturne_bin = runs_dir / "code_saturne" + saturne_bin.write_text("#!/bin/sh\n", encoding="utf-8") + saturne_bin.chmod(0o755) + + with pytest.raises(RuntimeError, match="1 case\\(s\\) not launched: case0001: No checkpoint"): + run_cases(runs_dir, 1, 1, 1, runtime="native", saturne_bin=str(saturne_bin), restart=True, source="test") + + registry = load_registry(runs_dir) + assert registry["case0001"]["status"] == STATUS_DONE # not stranded in PENDING + assert started == ["case0002", "launched"] + + +def test_cases_with_custom_ids_are_found(runs_dir: Path) -> None: + from csauto.registry import campaign_case_dirs + + for case_id in ("mesh.fine", "run-A"): + (runs_dir / case_id).mkdir() + (runs_dir / case_id / "doe_row.csv").write_text(f"case_id\n{case_id}\n", encoding="utf-8") + (runs_dir / "MESH").mkdir() + assert [p.name for p in campaign_case_dirs(runs_dir)] == ["mesh.fine", "run-A"] + + +def test_a_finished_child_is_dead_without_procfs(monkeypatch) -> None: + import subprocess + import sys + import time + + from csauto import runner + + proc = subprocess.Popen([sys.executable, "-c", "pass"]) + time.sleep(0.5) # exited, never reaped: a zombie + monkeypatch.setattr(runner, "_is_zombie", lambda pid: False) # what macOS reports + assert runner.is_process_alive(proc.pid) is False diff --git a/tests/unit/test_adapter_conformance.py b/tests/unit/test_adapter_conformance.py new file mode 100644 index 0000000..89cebe4 --- /dev/null +++ b/tests/unit/test_adapter_conformance.py @@ -0,0 +1,162 @@ +"""Contract checks every registered solver adapter must pass. + +Adding a solver? Register it in csauto/solvers/__init__.py, then add a +`sample_` function below that writes a small *finished* case the way +your solver leaves it (input file, logs, results). Every test here then runs +against your adapter, and tells you what the dashboard would miss. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from csauto.execution import RuntimeSelection +from csauto.logs import list_tail_files +from csauto.registry import STATUS_DONE +from csauto.runner import final_outcome +from csauto.solvers import available_solvers, get_solver_adapter +from csauto.solvers.base import ALL_DASHBOARD_PANELS, GENERIC_PANELS + + +def sample_code_saturne(case_dir: Path) -> None: + (case_dir / "DATA").mkdir(parents=True) + (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") + run_dir = case_dir / "RESU" / "20260101-1200" + (run_dir / "monitoring").mkdir(parents=True) + (run_dir / "profiles").mkdir() + (run_dir / "run_solver.log").write_text( + "Variable Rhs norm\n------\n 1 Velocity 1.0e-3\n\n END OF CALCULATION\n", encoding="utf-8" + ) + (run_dir / "residuals.csv").write_text("iteration,velocity,pressure\n1,1e-2,1e-3\n2,1e-3,1e-4\n", encoding="utf-8") + (run_dir / "monitoring" / "probes_Velocity.csv").write_text("t,1,2\n0.1,1.0,2.0\n0.2,1.1,2.1\n", encoding="utf-8") + (run_dir / "profiles" / "line_U.csv").write_text("s,U\n0.0,1.0\n1.0,1.2\n", encoding="utf-8") + (run_dir / "performance.log").write_text("total elapsed time: 12.5\nmpi ranks: 2\n", encoding="utf-8") + (run_dir / "summary").write_text("run summary\n", encoding="utf-8") + + +def sample_code_aster(case_dir: Path) -> None: + case_dir.mkdir(parents=True) + (case_dir / "study.export").write_text("F comm study.comm D 1\nF mess output.mess R 6\n", encoding="utf-8") + (case_dir / "study.comm").write_text("DEBUT()\nFIN()\n", encoding="utf-8") + (case_dir / "csauto.stdout").write_text("...\n--- DIAGNOSTIC JOB : OK\n", encoding="utf-8") + (case_dir / "output.mess").write_text("--- DIAGNOSTIC JOB : OK\n", encoding="utf-8") + (case_dir / "RESU").mkdir() + (case_dir / "RESU" / "results.rmed").write_bytes(b"\x89HDF\x00") + + +def sample_stub(case_dir: Path) -> None: + run_dir = case_dir / "OUT" / "run_0001" + run_dir.mkdir(parents=True) + (case_dir / "stub.toml").write_text("steps = 2\n", encoding="utf-8") + (run_dir / "stub.log").write_text("step 1\nstep 2\nSTUB CALCULATION COMPLETE\n", encoding="utf-8") + + +@pytest.fixture(params=available_solvers()) +def adapter(request): + return get_solver_adapter(request.param) + + +@pytest.fixture() +def finished_case(adapter, tmp_path: Path) -> Path: + sample = globals().get(f"sample_{adapter.name}") + assert sample, f"add a sample_{adapter.name}(case_dir) function to {__file__}" + case_dir = tmp_path / "RUNS" / "case0001" + sample(case_dir) + (case_dir / "doe_row.csv").write_text("case_id\ncase0001\n", encoding="utf-8") + for name in ("csauto.stdout", "csauto.stderr"): + if not (case_dir / name).exists(): + (case_dir / name).write_text("", encoding="utf-8") + return case_dir + + +def test_required_declarations(adapter) -> None: + for attribute in ("name", "native_bin_name", "results_dirname"): + assert getattr(adapter, attribute), f"{adapter.name}: {attribute} is required" + if "docker" in adapter.supported_runtimes: + assert adapter.default_docker_image, "docker is supported: declare default_docker_image" + if adapter.supported_runtimes & {"docker", "singularity"}: + assert adapter.container_bin_name, "container runtimes are supported: declare container_bin_name" + + +def test_declared_panels_are_known_ordered_and_feedable(adapter) -> None: + panels = adapter.dashboard_panels + assert set(panels) <= set(ALL_DASHBOARD_PANELS), "unknown panel name" + assert list(panels) == [p for p in ALL_DASHBOARD_PANELS if p in panels], "panels out of display order" + for panel in panels: + assert panel in GENERIC_PANELS or panel in adapter.capabilities, f"cannot feed the {panel!r} panel" + + +def test_capabilities_come_with_what_they_need(adapter) -> None: + caps = adapter.capabilities + if "performance" in caps: + assert adapter._provides("find_performance_log") and adapter._provides("parse_performance") + if "probes" in caps: + assert adapter._provides("locate_probe_files"), "list_probe_files needs locate_probe_files" + if "restart" in caps: + assert adapter.restart_modes, "build_restart_args needs restart_modes for the Restart dialog" + if "control" in caps: + assert adapter._provides("apply_control"), "control_actions need apply_control" + for options in (adapter.control_actions, adapter.restart_modes, adapter.compare_kinds): + for field in (0, 1): # names identify options, labels tell them apart in the dashboard + values = [option[field] for option in options] + assert len(values) == len(set(values)), f"duplicate names or labels in {options}" + + +def test_launch_command_for_every_supported_runtime(adapter, finished_case: Path) -> None: + argv = adapter.run_argv(finished_case, 2, 1) + assert all(isinstance(arg, str) for arg in argv) + selections = { + "native": RuntimeSelection("native", "image", saturne_bin="/usr/bin/solver"), + "docker": RuntimeSelection("docker", adapter.default_docker_image or "image"), + "singularity": RuntimeSelection("singularity", "image", singularity_bin="apptainer", singularity_image="a.sif"), + } + for runtime in adapter.supported_runtimes: + command = adapter.build_run_command(finished_case, 2, 1, selections[runtime]) + assert command and all(isinstance(part, str) for part in command), runtime + + +def test_finished_case_reads_as_done(adapter, finished_case: Path) -> None: + assert final_outcome(adapter, finished_case, None) == STATUS_DONE + + +def test_log_files_resolve(adapter, finished_case: Path) -> None: + tail_files = list_tail_files(finished_case, adapter) + assert tail_files, "the Log Tail panel would be empty" + for name in tail_files: + assert adapter.locate_case_file(finished_case, name), f"Log Tail offers {name!r} but cannot open it" + assert adapter.locate_case_file(finished_case, tail_files[0]) + + +def test_compare_kinds_resolve(adapter, finished_case: Path) -> None: + for kind in adapter.compare_kinds: + assert adapter.locate_case_file(finished_case, kind.value), f"compare kind {kind.value!r} not found" + + +def test_run_dirs_live_in_the_results_folder(adapter, finished_case: Path) -> None: + results_root = adapter.results_root(finished_case) + run_dirs = adapter.list_run_dirs(finished_case) + assert run_dirs, "a finished case should have at least one run folder (see list_run_dirs)" + for run_dir in run_dirs: + assert run_dir == results_root or run_dir.parent == results_root, "Clean deletes these folders" + for path in adapter.list_result_files(finished_case): + assert (finished_case / path).is_file() + + +def test_analytics_return_data_when_offered(adapter, finished_case: Path) -> None: + caps = adapter.capabilities + if "residuals" in caps: + files = adapter.find_residuals_files(finished_case) + rows = adapter.parse_live_residuals(finished_case)[1] + assert files or rows, "residuals offered but none found" + if "probes" in caps: + names = adapter.list_probe_files(finished_case) + assert names, "probes offered but none listed" + paths = adapter.locate_probe_files(finished_case, names[0]) + assert paths and adapter.read_probe_file(paths[0])[0], "probe listed but unreadable" + if "performance" in caps: + log = adapter.find_performance_log(finished_case) + assert log, "performance offered but no log found" + values = adapter.parse_performance(log) + assert any(values.get(key) for key in adapter.performance_fields), "no declared timing key parsed" diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 49508fd..5f5569f 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -187,23 +187,15 @@ def serve_fastapi_stub( @pytest.mark.parametrize( - ("flags", "expected_action", "expected_value"), - [ - (["--stop"], "stop", None), - (["--extend", "500"], "extend", 500), - (["--checkpoint"], "checkpoint", None), - (["--flush"], "flush", None), - ], + ("words", "expected_action", "expected_value"), + [(["stop"], "stop", None), (["extend", "500"], "extend", 500.0), (["pause", "0.5"], "pause", 0.5)], ) -def test_control_command_parses_action_flags(flags: list[str], expected_action: str, expected_value) -> None: - _parser, args = parse_arguments(["control", "RUNS", "case0007", *flags], Config()) +def test_control_command_parses_action_and_value(words: list[str], expected_action: str, expected_value) -> None: + _parser, args = parse_arguments(["control", "RUNS", "case0007", *words], Config()) assert args.command == "control" assert args.runs_dir == Path("RUNS") assert args.case == "case0007" - if expected_action == "extend": - assert args.extend == expected_value - else: - assert getattr(args, expected_action) is True + assert (args.action, args.value) == (expected_action, expected_value) def test_control_command_requires_an_action() -> None: @@ -211,9 +203,19 @@ def test_control_command_requires_an_action() -> None: parse_arguments(["control", "RUNS", "case0007"], Config()) -def test_control_command_rejects_multiple_actions() -> None: - with pytest.raises(SystemExit): - parse_arguments(["control", "RUNS", "case0007", "--stop", "--checkpoint"], Config()) +def test_control_command_takes_a_positional_action(tmp_path: Path, monkeypatch) -> None: + from csauto import cli as cli_module + + calls: list[tuple[str, object]] = [] + monkeypatch.setattr( + cli_module, + "control_case", + lambda _runs, _case, action, *, value=None, source="cli", adapter=None: calls.append((action, value)) or {}, + ) + runs_dir = tmp_path / "RUNS" + runs_dir.mkdir() + assert main(["control", str(runs_dir), "case0007", "extend", "50"]) == 0 + assert calls == [("extend", 50.0)] def test_control_command_invokes_control_case(tmp_path: Path) -> None: @@ -231,7 +233,7 @@ def control_case_stub(runs_dir_arg, case_id, action, *, value=None, source="cli" original_control_case = cli_module.control_case cli_module.control_case = control_case_stub try: - exit_code = main(["control", str(runs_dir), "case0007", "--extend", "500"]) + exit_code = main(["control", str(runs_dir), "case0007", "extend", "500"]) finally: cli_module.control_case = original_control_case diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 71d17dc..f46b680 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -4,6 +4,7 @@ import pytest +from csauto.cli import main from csauto.config import load_config @@ -40,6 +41,16 @@ def test_load_config_invalid_toml_raises(tmp_path: Path) -> None: load_config(cfg) +def test_cli_reports_invalid_toml_without_a_traceback(tmp_path: Path, monkeypatch, capsys) -> None: + (tmp_path / "csauto.toml").write_text('runtime = "native"\n[api\n', encoding="utf-8") + monkeypatch.chdir(tmp_path) + + assert main(["status", "RUNS"]) == 1 + err = capsys.readouterr().err + assert "Invalid TOML" in err + assert "Traceback" not in err + + def test_load_config_invalid_runtime_raises(tmp_path: Path) -> None: cfg = tmp_path / "csauto.toml" cfg.write_text('runtime = "podman"\n', encoding="utf-8") diff --git a/tests/unit/test_docker.py b/tests/unit/test_docker.py index 4dea8f2..1413c57 100644 --- a/tests/unit/test_docker.py +++ b/tests/unit/test_docker.py @@ -2,34 +2,58 @@ from pathlib import Path -from csauto.docker import build_gui_command, build_run_command, read_container_id +from csauto.docker import campaign_label, read_container_id +from csauto.execution import RuntimeSelection, build_runtime_gui_command, build_runtime_run_command +from csauto.solvers import get_solver_adapter + + +def _docker(image: str = "my_image") -> RuntimeSelection: + return RuntimeSelection(runtime="docker", docker_image=image) def test_build_run_command_without_display(monkeypatch, tmp_path: Path) -> None: monkeypatch.delenv("DISPLAY", raising=False) case_dir = tmp_path / "case0001" case_dir.mkdir() - cmd = build_run_command(case_dir, 4, 2, "my_image") - container_case = "/home/code_saturne/case0001" - assert cmd[:3] == ["nohup", "docker", "run"] - assert "--label" in cmd - assert "-w" in cmd - assert container_case in cmd - assert "run" in cmd - assert "--case" in cmd - assert cmd.index("run") < cmd.index("--case") - assert "-n" in cmd and cmd[cmd.index("-n") + 1] == "4" - assert "--nt" in cmd and cmd[cmd.index("--nt") + 1] == "2" + cmd = build_runtime_run_command(case_dir, 4, 2, _docker()) + assert cmd[:4] == ["nohup", "docker", "run", "--rm"] + assert cmd[cmd.index("-w") + 1] == "/csauto/case0001" + assert "csauto.case_id=case0001" in cmd + assert f"csauto.campaign={campaign_label(tmp_path)}" in cmd + image = cmd.index("my_image") + # --entrypoint makes the command independent of the image's own ENTRYPOINT. + assert cmd[image - 2 : image] == ["--entrypoint", "code_saturne"] + assert cmd[image + 1 :] == ["run", "--case", ".", "-n", "4", "--nt", "2"] def test_build_run_command_appends_extra_run_args(monkeypatch, tmp_path: Path) -> None: monkeypatch.delenv("DISPLAY", raising=False) case_dir = tmp_path / "case0001" case_dir.mkdir() - cmd = build_run_command(case_dir, 4, 2, "my_image", run_args=["--restart", "--iter-num", "250"]) + cmd = build_runtime_run_command(case_dir, 4, 2, _docker(), run_args=["--restart", "--iter-num", "250"]) assert cmd[-3:] == ["--restart", "--iter-num", "250"] +def test_container_setup_runs_before_the_solver(monkeypatch, tmp_path: Path) -> None: + monkeypatch.delenv("DISPLAY", raising=False) + case_dir = tmp_path / "case0001" + case_dir.mkdir() + cmd = build_runtime_run_command(case_dir, 2, 1, _docker("aster"), adapter=get_solver_adapter("code_aster")) + image = cmd.index("aster") + assert cmd[image - 2 : image] == ["--entrypoint", "bash"] + assert cmd[image + 1 :] == [ + "-c", + '[ ! -f /opt/activate.sh ] || source /opt/activate.sh && exec "$0" "$@"', + "run_aster", + ".csauto.export", + ] + + +def test_campaign_labels_differ_between_campaigns(tmp_path: Path) -> None: + assert campaign_label(tmp_path / "A") != campaign_label(tmp_path / "B") + assert campaign_label(tmp_path / "A") == campaign_label(tmp_path / "A") + + def test_read_container_id_returns_first_nonempty_line(tmp_path: Path) -> None: """Fix 6: blank leading lines must not cause a premature empty-string return.""" cidfile = tmp_path / "container.cid" @@ -65,7 +89,7 @@ def test_build_run_command_mounts_symlinked_shared_dirs(monkeypatch, tmp_path: P (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True) (runs_dir / "POST").symlink_to(post, target_is_directory=True) - cmd = build_run_command(case_dir, 4, 2, "my_image") + cmd = build_runtime_run_command(case_dir, 4, 2, _docker()) joined = " ".join(cmd) assert f"-v {mesh.resolve()}:{mesh.resolve()}:ro" in joined @@ -87,10 +111,11 @@ def test_build_gui_command_mounts_shared_dir_symlinks(monkeypatch, tmp_path: Pat (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True) (runs_dir / "POST").symlink_to(post, target_is_directory=True) - cmd = build_gui_command(case_dir, "my_image") + cmd = build_runtime_gui_command(case_dir, _docker()) assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd assert f"{post.resolve()}:{post.resolve()}" in cmd + assert cmd[-2:] == ["gui", "DATA/setup.xml"] def test_build_gui_command_adds_no_mount_without_symlinks(monkeypatch, tmp_path: Path) -> None: @@ -101,6 +126,43 @@ def test_build_gui_command_adds_no_mount_without_symlinks(monkeypatch, tmp_path: (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") (runs_dir / "MESH").mkdir() - cmd = build_gui_command(case_dir, "my_image") + cmd = build_runtime_gui_command(case_dir, _docker()) assert cmd.count("-v") == 1 + + +def test_image_entrypoint_keeps_the_images_own_entrypoint(monkeypatch, tmp_path: Path) -> None: + import subprocess + + import csauto.docker as docker_module + from csauto.solvers import get_solver_adapter + + entrypoints = { + "stock": '["code_saturne"]', + "wrapper": '["/entrypoint.sh"]', + "tini": '["tini","--"]', + "bare": "null", + } + probes: list[str] = [] + + def fake_docker(*args, timeout): + if args[:2] == ("image", "inspect"): + return subprocess.CompletedProcess(args, 0, f"sha-{args[-1]} {entrypoints[args[-1]]}\n", "") + probes.append(args[-2]) + return subprocess.CompletedProcess(args, 0 if args[-2] == "sha-tini" else 1, "", "") + + monkeypatch.setattr(docker_module, "_docker", fake_docker) + monkeypatch.setattr(docker_module.shutil, "which", lambda name: "/usr/bin/docker") + docker_module._probe_entrypoint.cache_clear() + adapter = get_solver_adapter("code_saturne") + + def image_part(image: str) -> list[str]: + cmd = docker_module.build_run_command(tmp_path / "RUNS" / "case0001", image, ["run"], adapter=adapter) + return cmd[cmd.index("-w") + 2 :] + + assert image_part("stock") == ["stock", "run"] # starts the solver: no probe needed + assert image_part("wrapper") == ["wrapper", "run"] # its script starts the solver + assert image_part("tini") == ["tini", "code_saturne", "run"] # runs the command it is given + assert image_part("bare") == ["--entrypoint", "code_saturne", "bare", "run"] + image_part("wrapper") + assert probes == ["sha-wrapper", "sha-tini"] # once per image diff --git a/tests/unit/test_doe.py b/tests/unit/test_doe.py index b16e2cc..52ee96e 100644 --- a/tests/unit/test_doe.py +++ b/tests/unit/test_doe.py @@ -76,7 +76,9 @@ def test_generate_cases_missing_column_error_message(tmp_path: Path) -> None: with pytest.raises(ValueError) as excinfo: generate_cases(headers, rows, template_dir, output_dir) - assert str(excinfo.value) == "Variables without matching DOE columns: bar" + message = str(excinfo.value) + assert message.startswith("Variables without matching DOE columns: bar in setup.xml.") + assert "\\{bar}" in message def test_generate_cases_duplicate_case_id_raises(tmp_path: Path) -> None: @@ -93,3 +95,86 @@ def test_generate_cases_duplicate_case_id_raises(tmp_path: Path) -> None: with pytest.raises(ValueError) as excinfo: generate_cases(headers, rows, template_dir, output_dir) assert str(excinfo.value) == "Duplicate case_id in DOE: caseA" + + +def test_generate_cases_keeps_non_utf8_bytes_and_binaries(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + template_dir = tmp_path / "TEMPLATE" + template_dir.mkdir() + (template_dir / "study.export").write_text("F comm study.comm D 1\n", encoding="utf-8") + (template_dir / "study.comm").write_bytes("# débit imposé\nQ = {inflow}\n".encode("latin-1")) + (template_dir / "mesh.med").write_bytes(b"\x89HDF\x00{inflow}\xff") + + generate_cases( + ["inflow"], [{"inflow": "2.5"}], template_dir, tmp_path / "RUNS", adapter=get_solver_adapter("code_aster") + ) + + case_dir = tmp_path / "RUNS" / "case0001" + assert (case_dir / "study.comm").read_bytes() == "# débit imposé\nQ = 2.5\n".encode("latin-1") + assert (case_dir / "mesh.med").read_bytes() == b"\x89HDF\x00{inflow}\xff" + # Rendering is stable, so preparing again recognises the existing case. + generate_cases( + ["inflow"], [{"inflow": "2.5"}], template_dir, tmp_path / "RUNS", adapter=get_solver_adapter("code_aster") + ) + + +def test_missing_column_error_names_the_file_and_the_escape(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + template_dir = tmp_path / "TEMPLATE" + template_dir.mkdir() + (template_dir / "study.export").write_text("F comm study.comm D 1\n", encoding="utf-8") + (template_dir / "study.comm").write_text("print(f'step {i}')\n", encoding="utf-8") + with pytest.raises(ValueError) as excinfo: + generate_cases(["x"], [{"x": "1"}], template_dir, tmp_path / "RUNS", adapter=get_solver_adapter("code_aster")) + assert "i in study.comm" in str(excinfo.value) + assert "\\{i}" in str(excinfo.value) + + +def test_escaped_braces_are_unescaped_even_without_placeholders(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + template_dir = tmp_path / "TEMPLATE" + template_dir.mkdir() + (template_dir / "study.export").write_text("F comm study.comm D 1\nP x {x}\n", encoding="utf-8") + (template_dir / "study.comm").write_text("print(f'step \\{i}')\n", encoding="utf-8") + generate_cases(["x"], [{"x": "1"}], template_dir, tmp_path / "RUNS", adapter=get_solver_adapter("code_aster")) + assert (tmp_path / "RUNS" / "case0001" / "study.comm").read_text(encoding="utf-8") == "print(f'step {i}')\n" + + +def test_a_bad_doe_value_leaves_no_half_made_case(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + template_dir = tmp_path / "TEMPLATE" + template_dir.mkdir() + (template_dir / "stub.toml").write_text("steps = {steps}\n", encoding="utf-8") + runs = tmp_path / "RUNS" + with pytest.raises(ValueError, match="Missing placeholder values"): + generate_cases(["steps"], [{"steps": ""}], template_dir, runs, adapter=get_solver_adapter("stub")) + assert not (runs / "case0001").exists() + generate_cases(["steps"], [{"steps": "3"}], template_dir, runs, adapter=get_solver_adapter("stub")) + assert (runs / "case0001" / "stub.toml").read_text(encoding="utf-8") == "steps = 3\n" + + +def test_cases_written_by_older_versions_still_match(tmp_path: Path) -> None: + template_dir = tmp_path / "TEMPLATE" + (template_dir / "DATA").mkdir(parents=True) + (template_dir / "DATA" / "setup.xml").write_bytes(b"\r\n {foo}\r\n \r\n\r\n") + output_dir = tmp_path / "RUNS" + generate_cases(["foo"], [{"foo": "1"}], template_dir, output_dir) + setup = output_dir / "case0001" / "DATA" / "setup.xml" + assert setup.read_bytes() == b"\r\n 1\r\n \r\n\r\n" + # csauto 0.5 wrote LF line endings and dropped the bytes that were not UTF-8. + setup.write_bytes(b"\n 1\n \n\n") + generate_cases(["foo"], [{"foo": "1"}], template_dir, output_dir) + + +def test_a_binary_with_a_text_head_is_copied_verbatim(tmp_path: Path) -> None: + template_dir = tmp_path / "TEMPLATE" + (template_dir / "DATA").mkdir(parents=True) + (template_dir / "DATA" / "setup.xml").write_text("{foo}", encoding="utf-8") + blob = b"{foo}\n" * 2000 + b"\x00\xff" # the NUL byte comes after the 8 KiB sniff + (template_dir / "mesh.bin").write_bytes(blob) + generate_cases(["foo"], [{"foo": "1"}], template_dir, tmp_path / "RUNS") + assert (tmp_path / "RUNS" / "case0001" / "mesh.bin").read_bytes() == blob diff --git a/tests/unit/test_execution.py b/tests/unit/test_execution.py index 706914a..670273c 100644 --- a/tests/unit/test_execution.py +++ b/tests/unit/test_execution.py @@ -57,9 +57,8 @@ def test_build_runtime_run_command_native(tmp_path: Path) -> None: saturne_bin="/opt/code_saturne/bin/code_saturne", ) cmd = build_runtime_run_command(case_dir, nprocs=4, nt=2, selection=selection) - assert cmd[:3] == ["nohup", "/opt/code_saturne/bin/code_saturne", "run"] - assert "--case" in cmd - assert str(case_dir) in cmd + # Every runtime starts inside the case folder, so the case is ".". + assert cmd[:5] == ["nohup", "/opt/code_saturne/bin/code_saturne", "run", "--case", "."] assert "-n" in cmd and cmd[cmd.index("-n") + 1] == "4" assert "--nt" in cmd and cmd[cmd.index("--nt") + 1] == "2" @@ -109,13 +108,13 @@ def test_build_runtime_run_command_singularity_slurm_uses_cleanenv_and_env(tmp_p "code_saturne", "run", "--case", - "/home/code_saturne/case0001", + ".", "-n", "4", "--nt", "2", ] - assert "--pwd" in cmd and cmd[cmd.index("--pwd") + 1] == "/home/code_saturne/case0001" + assert "--pwd" in cmd and cmd[cmd.index("--pwd") + 1] == "/csauto/case0001" def test_build_runtime_gui_command_native(tmp_path: Path) -> None: @@ -128,11 +127,7 @@ def test_build_runtime_gui_command_native(tmp_path: Path) -> None: saturne_bin="/opt/code_saturne/bin/code_saturne", ) cmd = build_runtime_gui_command(case_dir, selection) - assert cmd == [ - "/opt/code_saturne/bin/code_saturne", - "gui", - str(case_dir / "DATA" / "setup.xml"), - ] + assert cmd == ["/opt/code_saturne/bin/code_saturne", "gui", "DATA/setup.xml"] def test_build_runtime_gui_command_singularity(tmp_path: Path, monkeypatch) -> None: @@ -147,14 +142,13 @@ def test_build_runtime_gui_command_singularity(tmp_path: Path, monkeypatch) -> N singularity_image="/images/code_saturne.sif", ) cmd = build_runtime_gui_command(case_dir, selection) - container_case = "/home/code_saturne/case0001" - container_setup = f"{container_case}/DATA/setup.xml" + container_case = "/csauto/case0001" assert cmd[:2] == ["/usr/bin/apptainer", "exec"] assert "--bind" in cmd - assert f"{case_dir.parent.resolve()}:/home/code_saturne" in cmd + assert f"{case_dir.parent.resolve()}:/csauto" in cmd assert "--pwd" in cmd assert container_case in cmd - assert cmd[-4:] == ["/images/code_saturne.sif", "code_saturne", "gui", container_setup] + assert cmd[-4:] == ["/images/code_saturne.sif", "code_saturne", "gui", "DATA/setup.xml"] def test_build_singularity_slurm_script_uses_stage_solver_finalize(tmp_path: Path) -> None: @@ -200,15 +194,15 @@ def test_check_shared_dir_symlinks_native_is_always_ok(tmp_path: Path) -> None: outside.mkdir(parents=True) (runs_dir / "MESH").symlink_to(outside, target_is_directory=True) - check_shared_dir_symlinks(runs_dir, RUNTIME_NATIVE) + check_shared_dir_symlinks(runs_dir, RUNTIME_NATIVE, ("MESH", "POST")) def test_check_shared_dir_symlinks_no_symlink_is_ok_for_containers(tmp_path: Path) -> None: runs_dir = tmp_path / "RUNS" (runs_dir / "MESH").mkdir(parents=True) - check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER) - check_shared_dir_symlinks(runs_dir, RUNTIME_SINGULARITY) + check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER, ("MESH", "POST")) + check_shared_dir_symlinks(runs_dir, RUNTIME_SINGULARITY, ("MESH", "POST")) def test_check_shared_dir_symlinks_inside_runs_dir_is_ok_for_containers(tmp_path: Path) -> None: @@ -217,7 +211,7 @@ def test_check_shared_dir_symlinks_inside_runs_dir_is_ok_for_containers(tmp_path real_mesh.mkdir(parents=True) (runs_dir / "MESH").symlink_to(real_mesh, target_is_directory=True) - check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER) + check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER, ("MESH", "POST")) @pytest.mark.parametrize("runtime", [RUNTIME_DOCKER, RUNTIME_SINGULARITY]) @@ -228,7 +222,7 @@ def test_check_shared_dir_symlinks_outside_runs_dir_is_ok_for_containers(tmp_pat outside.mkdir(parents=True) (runs_dir / "MESH").symlink_to(outside, target_is_directory=True) - check_shared_dir_symlinks(runs_dir, runtime) + check_shared_dir_symlinks(runs_dir, runtime, ("MESH", "POST")) @pytest.mark.parametrize("runtime", [RUNTIME_DOCKER, RUNTIME_SINGULARITY]) @@ -238,7 +232,7 @@ def test_check_shared_dir_symlinks_raises_for_broken_symlink(tmp_path: Path, run (runs_dir / "MESH").symlink_to(tmp_path / "gone", target_is_directory=True) with pytest.raises(RuntimeError, match="does not exist"): - check_shared_dir_symlinks(runs_dir, runtime) + check_shared_dir_symlinks(runs_dir, runtime, ("MESH", "POST")) def test_shared_dir_symlink_mounts_lists_outside_targets(tmp_path: Path) -> None: @@ -253,7 +247,7 @@ def test_shared_dir_symlink_mounts_lists_outside_targets(tmp_path: Path) -> None (runs_dir / "MESH").symlink_to(mesh, target_is_directory=True) (runs_dir / "POST").symlink_to(post, target_is_directory=True) - mounts = shared_dir_symlink_mounts(runs_dir) + mounts = shared_dir_symlink_mounts(runs_dir, ("MESH", "POST"), {"MESH"}) assert (mesh.resolve(), True) in mounts assert (post.resolve(), False) in mounts @@ -268,7 +262,7 @@ def test_shared_dir_symlink_mounts_ignores_real_dirs_and_internal_links(tmp_path (runs_dir / "MESH").symlink_to(real_mesh, target_is_directory=True) (runs_dir / "POST").mkdir() - assert shared_dir_symlink_mounts(runs_dir) == [] + assert shared_dir_symlink_mounts(runs_dir, ("MESH", "POST"), {"MESH"}) == [] def test_build_runtime_run_command_singularity_binds_symlinked_shared_dirs(tmp_path: Path) -> None: @@ -335,3 +329,14 @@ def test_build_runtime_gui_command_singularity_mounts_shared_dir_symlinks(tmp_pa assert f"{mesh.resolve()}:{mesh.resolve()}:ro" in cmd assert f"{post.resolve()}:{post.resolve()}" in cmd + + +def test_check_shared_dir_symlinks_refuses_targets_under_the_container_root(tmp_path: Path) -> None: + runs_dir = tmp_path / "RUNS" + runs_dir.mkdir() + mesh = tmp_path / "data" / "MESH" + mesh.mkdir(parents=True) + (runs_dir / "MESH").symlink_to(mesh) + with pytest.raises(RuntimeError, match="collides with the campaign mount point"): + check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER, ("MESH",), container_root=str(mesh.parent.resolve())) + check_shared_dir_symlinks(runs_dir, RUNTIME_DOCKER, ("MESH",), container_root="/csauto") diff --git a/tests/unit/test_logs.py b/tests/unit/test_logs.py index 722a6f2..2799c95 100644 --- a/tests/unit/test_logs.py +++ b/tests/unit/test_logs.py @@ -5,12 +5,21 @@ from datetime import datetime from pathlib import Path -from csauto.logs import detect_run_outcome, highlight_anomaly_line +from csauto.logs import GENERIC_ANOMALY_PATTERNS, highlight_anomaly_line from csauto.registry import STATUS_DONE, STATUS_FAILED +from csauto.solvers import get_solver_adapter + +CODE_SATURNE = get_solver_adapter("code_saturne") +CS_PATTERNS = (*GENERIC_ANOMALY_PATTERNS, *CODE_SATURNE.anomaly_patterns) + + +def detect_run_outcome(case_dir: Path, start_time: str | None = None) -> str | None: + return CODE_SATURNE.detect_outcome(case_dir, start_time) def test_highlight_anomaly_ignores_no_error() -> None: - assert highlight_anomaly_line("No error detected") is None + assert highlight_anomaly_line("No error detected", CS_PATTERNS, CODE_SATURNE.anomaly_ignore_patterns) is None + assert highlight_anomaly_line("No error detected") is not None def test_highlight_anomaly_warn() -> None: @@ -21,6 +30,13 @@ def test_highlight_anomaly_warn() -> None: assert "err-hit" in html +def test_cfd_warnings_come_from_the_adapter() -> None: + assert highlight_anomaly_line("clipping of k") is None + result = highlight_anomaly_line("clipping of k", CS_PATTERNS) + assert result is not None + assert result[1] == "warn" + + def test_detect_run_outcome_done(tmp_path: Path) -> None: case_dir = tmp_path / "case0001" case_dir.mkdir() @@ -73,3 +89,40 @@ def locate_case_file(self, case_dir, name): items = collect_recent_errors(runs_dir, ["case0001"], adapter=adapter) assert len(items) == 1 + + +def test_tail_prints_the_requested_number_of_lines(tmp_path: Path, capsys) -> None: + from csauto.logs import tail_log + from csauto.solvers import get_solver_adapter + + case_dir = tmp_path / "RUNS" / "case0001" + case_dir.mkdir(parents=True) + (case_dir / "csauto.stdout").write_text("".join(f"line {i:04d} {'x' * 70}\n" for i in range(500)), encoding="utf-8") + tail_log( + tmp_path / "RUNS", "case0001", "csauto.stdout", lines=200, follow=False, adapter=get_solver_adapter("stub") + ) + printed = capsys.readouterr().out.splitlines()[1:] + assert len(printed) == 200 + assert printed[0].startswith("line 0300") and printed[-1].startswith("line 0499") + + +def test_tail_files_come_from_the_current_run_only(tmp_path: Path) -> None: + from csauto.logs import list_tail_files + + adapter = get_solver_adapter("code_saturne") + case_dir = tmp_path / "case0001" + old_run = case_dir / "RESU" / "20260101-1000" + old_run.mkdir(parents=True) + for name in ("run_solver.log", "summary", "performance.log"): + (old_run / name).write_text("done\n", encoding="utf-8") + (case_dir / "csauto.stdout").write_text("", encoding="utf-8") + assert list_tail_files(case_dir, adapter)[0] == "run_solver.log" + + new_run = case_dir / "RESU" / "20260101-1100" # staging, no solver log yet + new_run.mkdir() + (new_run / "preprocessor.log").write_text("", encoding="utf-8") + os.utime(old_run, (time.time() - 60, time.time() - 60)) + + files = list_tail_files(case_dir, adapter) + assert "run_solver.log" not in files and "summary" not in files + assert "RESU/20260101-1100/preprocessor.log" in files diff --git a/tests/unit/test_maintenance.py b/tests/unit/test_maintenance.py index 746937c..6b10328 100644 --- a/tests/unit/test_maintenance.py +++ b/tests/unit/test_maintenance.py @@ -16,6 +16,7 @@ def _make_runs_dir(tmp_path: Path) -> Path: def _add_case(runs_dir: Path, case_id: str, with_setup: bool = True) -> Path: case_dir = runs_dir / case_id case_dir.mkdir(parents=True, exist_ok=True) + (case_dir / "doe_row.csv").write_text(f"case_id\n{case_id}\n", encoding="utf-8") if with_setup: (case_dir / "DATA").mkdir(parents=True, exist_ok=True) (case_dir / "DATA" / "setup.xml").write_text("", encoding="utf-8") @@ -48,7 +49,7 @@ def test_run_doctor_fails_when_no_case_directory_found(tmp_path: Path) -> None: runs_dir = _make_runs_dir(tmp_path) items = run_doctor(runs_dir, check_display=False) assert _has_item(items, "ok", "write OK") - assert _has_item(items, "fail", "no case* directory found") + assert _has_item(items, "fail", "no case found") def test_run_doctor_detects_case_directories(tmp_path: Path) -> None: @@ -324,7 +325,7 @@ def test_run_doctor_runtime_auto_warns_when_none_detected( _patch_which(monkeypatch, {}) items = run_doctor(runs_dir, runtime="auto", check_display=False) - assert _has_item(items, "warn", "no runtime found (code_saturne, docker, apptainer/singularity)") + assert _has_item(items, "warn", "no runtime found for code_saturne (code_saturne, docker, apptainer/singularity)") def test_run_doctor_runtime_auto_detects_native_from_saturne_bin_file( @@ -492,15 +493,16 @@ def test_doctor_reports_derived_panels_and_capabilities(tmp_path: Path) -> None: def test_doctor_says_none_when_the_solver_has_no_capability(tmp_path: Path) -> None: - from csauto.solvers.base import SolverAdapterBase + from csauto.solvers.base import SolverAdapter - class BareAdapter(SolverAdapterBase): + class BareAdapter(SolverAdapter): name = "bare" native_bin_name = "bare" container_bin_name = "bare" container_root = "/bare" default_docker_image = "" results_dirname = "OUT" + dashboard_panels = ("status", "tail", "errors") def run_argv(self, case_path, nprocs, nt, run_args=None): return [] @@ -516,3 +518,80 @@ def find_setup_file(self, template_dir): items = run_doctor(runs_dir, check_setup=False, check_display=False, adapter=BareAdapter()) assert _has_item(items, "ok", "solver bare: capabilities none") + + +def test_cleanup_only_deletes_the_folders_the_adapter_counts_as_runs(tmp_path: Path) -> None: + from csauto.solvers.code_saturne import CodeSaturneAdapter + + class RunsOnly(CodeSaturneAdapter): + def list_run_dirs(self, case_dir, *, newest_first=True): + return sorted((p for p in self.results_root(case_dir).glob("run_*")), reverse=newest_first) + + runs_dir = _make_runs_dir(tmp_path) + case_dir = _add_case(runs_dir, "case0001") + for name in ("run_1", "run_2", "restart_db"): + (case_dir / "RESU" / name).mkdir(parents=True) + + report = cleanup_runs(runs_dir, prune_resu=True, keep_last=0, adapter=RunsOnly()) + + assert report.resu_removed == 2 + assert [p.name for p in (case_dir / "RESU").iterdir()] == ["restart_db"] + + +def test_cleanup_skips_running_cases(tmp_path: Path) -> None: + from csauto.registry import save_registry + + runs_dir = _make_runs_dir(tmp_path) + case_dir = _add_case(runs_dir, "case0001") + (case_dir / "RESU" / "run_1").mkdir(parents=True) + log = case_dir / "csauto.stdout" + log.write_text("x" * 4096, encoding="utf-8") + save_registry(runs_dir, {"case0001": {"case_id": "case0001", "path": str(case_dir), "status": "RUNNING"}}) + + report = cleanup_runs(runs_dir, prune_resu=True, keep_last=0, max_log_mb=0.001) + + assert report.resu_removed == 0 and report.logs_truncated == 0 + assert (case_dir / "RESU" / "run_1").is_dir() + assert log.stat().st_size == 4096 + + +def test_code_aster_clean_keeps_its_single_run_until_delete_all(tmp_path: Path) -> None: + from csauto.solvers import get_solver_adapter + + runs_dir = _make_runs_dir(tmp_path) + case_dir = _add_case(runs_dir, "case0001", with_setup=False) + (case_dir / "RESU" / "base").mkdir(parents=True) + (case_dir / "RESU" / "results.rmed").write_bytes(b"\x00") + adapter = get_solver_adapter("code_aster") + + assert cleanup_runs(runs_dir, prune_resu=True, keep_last=1, adapter=adapter).resu_removed == 0 + assert (case_dir / "RESU" / "base").is_dir() + assert cleanup_runs(runs_dir, prune_resu=True, keep_last=0, adapter=adapter).resu_removed == 1 + assert not (case_dir / "RESU").exists() + assert (case_dir / "doe_row.csv").is_file() + + +def test_keeping_folders_a_case_does_not_have_leaves_that_case_alone(tmp_path: Path) -> None: + runs_dir = _make_runs_dir(tmp_path) + case_a = _add_case(runs_dir, "caseA") + case_b = _add_case(runs_dir, "caseB") + for case_dir, runs in ((case_a, ("a1", "a2")), (case_b, ("b1", "b2"))): + for run in runs: + (case_dir / "RESU" / run).mkdir(parents=True) + + report = cleanup_runs(runs_dir, prune_resu=True, keep_resu=["b2"]) + + assert report.resu_removed == 1 + assert sorted(p.name for p in (case_a / "RESU").iterdir()) == ["a1", "a2"] + assert [p.name for p in (case_b / "RESU").iterdir()] == ["b2"] + + +def test_cleanup_reports_the_cases_it_skipped(tmp_path: Path) -> None: + from csauto.registry import save_registry + + runs_dir = _make_runs_dir(tmp_path) + _add_case(runs_dir, "case0001") + _add_case(runs_dir, "case0002") + save_registry(runs_dir, {"case0001": {"case_id": "case0001", "status": "PENDING"}}) + + assert cleanup_runs(runs_dir, prune_resu=True).skipped_active == ["case0001"] diff --git a/tests/unit/test_performance.py b/tests/unit/test_performance.py index 7f6dcbf..57140c9 100644 --- a/tests/unit/test_performance.py +++ b/tests/unit/test_performance.py @@ -3,7 +3,10 @@ import csv from pathlib import Path -from csauto.logs import collect_performance, parse_performance_log, read_performance_rows +from csauto.logs import collect_performance, read_performance_rows +from csauto.solvers import get_solver_adapter + +parse_performance_log = get_solver_adapter("code_saturne").parse_performance def _write_performance_log(case_dir: Path, content: str) -> None: diff --git a/tests/unit/test_solver_boundary.py b/tests/unit/test_solver_boundary.py index 1cf30c4..88e5c78 100644 --- a/tests/unit/test_solver_boundary.py +++ b/tests/unit/test_solver_boundary.py @@ -1,38 +1,85 @@ """Enforce the solver adapter boundary. -Generic orchestration modules must not reference code_saturne conventions -directly; all solver-specific knowledge goes through the SolverAdapter. If -this test fails, route the new call through the adapter (or extend it) instead -of adding the literal back. +Modules outside csauto/solvers/ hold generic mechanism only; every +code_saturne convention (file name, directory layout, log marker) is +vocabulary declared by its adapter. If this test fails, route the new call +through the adapter (or extend it) instead of adding the literal back. """ from __future__ import annotations +import re from pathlib import Path import pytest PACKAGE_DIR = Path(__file__).resolve().parents[2] / "csauto" -SOLVER_TOKENS = ("code_saturne", "setup.xml", "run.cfg", "RESU", "MESH") +# Matched as whole words, where "_" separates words: "saturne" catches +# code_saturne and resolve_saturne, "RESU" does not catch RESULTS. +SOLVER_TOKENS = ( + "saturne", + "setup.xml", + "run.cfg", + "RESU", + "MESH", + "POST", + "listing", + "monitoring", + "profiles", + "run_solver.log", + "performance.log", + "run_status", + "residuals.csv", +) +TOKEN_RE = re.compile(r"(? None: - source = module_path.read_text(encoding="utf-8") - hits = [ - f"{module_path.name}:{lineno}: {line.strip()}" - for lineno, line in enumerate(source.splitlines(), start=1) - for token in SOLVER_TOKENS - if token in line - ] + rel = module_path.relative_to(PACKAGE_DIR).as_posix() + residue = (*ACCEPTED_RESIDUE["*"], *ACCEPTED_RESIDUE.get(rel, ())) + hits: list[str] = [] + for lineno, line in enumerate(module_path.read_text(encoding="utf-8").splitlines(), start=1): + scrubbed = line + for allowed in residue: + scrubbed = scrubbed.replace(allowed, "") + if TOKEN_RE.search(scrubbed): + hits.append(f"{rel}:{lineno}: {line.strip()}") assert not hits, "solver-specific literals leaked into generic modules:\n" + "\n".join(hits) + + +def test_accepted_residue_names_existing_modules() -> None: + for rel in ACCEPTED_RESIDUE: + assert rel == "*" or (PACKAGE_DIR / rel).is_file(), rel + + +# The dashboard source gets the same scan. Its residue: the HTTP verb POST, and +# the Probes panel's Profiles view, a generic view fed by list_profile_files. +FRONTEND_DIR = PACKAGE_DIR.parent / "frontend" / "src" +FRONTEND_FILES = sorted(p for p in FRONTEND_DIR.rglob("*") if p.suffix in {".svelte", ".ts", ".js"}) +FRONTEND_RESIDUE = ("POST", "profiles") + + +@pytest.mark.parametrize("path", FRONTEND_FILES, ids=lambda p: p.relative_to(FRONTEND_DIR).as_posix()) +def test_dashboard_source_has_no_solver_literals(path: Path) -> None: + hits: list[str] = [] + for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + scrubbed = line + for allowed in FRONTEND_RESIDUE: + scrubbed = scrubbed.replace(allowed, "") + if TOKEN_RE.search(scrubbed): + hits.append(f"{path.relative_to(FRONTEND_DIR)}:{lineno}: {line.strip()}") + assert not hits, "solver-specific literals in the dashboard source:\n" + "\n".join(hits) diff --git a/tests/unit/test_solvers.py b/tests/unit/test_solvers.py index 6dd4b36..49bae44 100644 --- a/tests/unit/test_solvers.py +++ b/tests/unit/test_solvers.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os from datetime import datetime +from pathlib import Path import pytest @@ -80,12 +82,13 @@ def test_conventions(self, adapter): assert adapter.shared_dir_names == ("MESH", "POST") assert adapter.template_input_names == {"setup.xml", "run.cfg"} assert adapter.default_docker_image == "simvia/code_saturne" - assert adapter.container_root == "/home/code_saturne" + assert adapter.container_root == "/csauto" assert adapter.default_compare_kind == "setup.xml" - assert adapter.control_actions == {"stop", "extend", "checkpoint", "flush"} + assert [a.name for a in adapter.control_actions] == ["stop", "extend", "checkpoint", "flush"] + assert adapter.control_action("extend").value_label def test_run_argv(self, adapter): - assert adapter.run_argv("/runs/case1", 2, 4) == ["run", "--case", "/runs/case1", "-n", "2", "--nt", "4"] + assert adapter.run_argv(Path("/runs/case1"), 2, 4) == ["run", "--case", ".", "-n", "2", "--nt", "4"] def test_run_argv_with_extra_args(self, adapter): argv = adapter.run_argv("/runs/case1", 1, 1, run_args=["--parametric-args=--restart=x", ""]) @@ -93,7 +96,7 @@ def test_run_argv_with_extra_args(self, adapter): assert "" not in argv def test_gui_argv(self, adapter): - assert adapter.gui_argv("/runs/case1/setup.xml") == ["gui", "/runs/case1/setup.xml"] + assert adapter.gui_argv("DATA/setup.xml") == ["gui", "DATA/setup.xml"] def test_build_run_command_matches_execution_native(self, adapter, tmp_path): selection = RuntimeSelection(runtime="native", docker_image="img", saturne_bin="/bin/true") @@ -144,86 +147,86 @@ class TestCodeAsterAdapter: def adapter(self): return get_solver_adapter("code_aster") - def test_shared_dir_mounts_keep_their_names(self, adapter, tmp_path): - runs_dir = tmp_path / "RUNS" - case_dir = runs_dir / "case1" - case_dir.mkdir(parents=True) - (case_dir / "study.export").write_text("P time_limit 300\n", encoding="utf-8") - (runs_dir / "MESH").mkdir() - resu_target = tmp_path / "resu_store" - resu_target.mkdir() - (runs_dir / "RESU").symlink_to(resu_target, target_is_directory=True) - - selection = RuntimeSelection(runtime="docker", docker_image="img") - script = adapter.build_run_command(case_dir, 1, 1, selection)[-1] - - assert f"{resu_target.resolve()}:/home/user/case1/RESU" in script - assert "/home/user/case1/MESH" not in script - - def test_singularity_command_cleans_case_local_tmp_dirs(self, adapter, tmp_path): - case_dir = tmp_path / "RUNS" / "case1" - case_dir.mkdir(parents=True) - (case_dir / "study.export").write_text("P time_limit 300\n", encoding="utf-8") - selection = RuntimeSelection( - runtime="singularity", - docker_image="img", - singularity_image="/images/aster.sif", - singularity_bin="apptainer", - ) - - script = adapter.build_run_command(case_dir, 1, 1, selection)[-1] - - assert script.count("rm -rf") == 1 - assert f"{case_dir.resolve()}/.apptainer_tmp" in script - assert f"{case_dir.resolve()}/TMP" in script - assert "~" not in script - - def test_relaunch_does_not_duplicate_the_mess_entry(self, adapter, tmp_path): + def test_launch_runs_a_copy_of_the_export_with_n_and_nt(self, adapter, tmp_path): case_dir = tmp_path / "RUNS" / "case1" case_dir.mkdir(parents=True) export = case_dir / "cube.export" - export.write_text("P time_limit 300\nF comm study.comm D 1", encoding="utf-8") + export.write_text("P ncpus 1\nP mpi_nbcpu 1\nF comm study.comm D 1\n", encoding="utf-8") - selection = RuntimeSelection(runtime="docker", docker_image="img") - for _ in range(2): - script = adapter.build_run_command(case_dir, 1, 1, selection)[-1] + adapter.prepare_launch(case_dir, 4, 2) - assert "run_aster cube.export" in script - content = export.read_text(encoding="utf-8") - assert content.count("F mess RESU/LOGS/run_solver.log R 6") == 1 - assert "D 1\nF mess" in content + run_export = (case_dir / ".csauto.export").read_text(encoding="utf-8") + assert run_export.splitlines() == ["F comm study.comm D 1", "P mpi_nbcpu 4", "P ncpus 2"] + assert export.read_text(encoding="utf-8").startswith("P ncpus 1") # the case's export is untouched + assert (case_dir / "RESU").is_dir() + assert adapter.run_argv(case_dir, 4, 2) == [".csauto.export"] + # The copy is hidden, so it never counts as a second export. + assert adapter.find_setup_file(case_dir) == export - def test_build_run_command_requires_an_export_file(self, adapter, tmp_path): - case_dir = tmp_path / "RUNS" / "case1" - case_dir.mkdir(parents=True) - selection = RuntimeSelection(runtime="docker", docker_image="img") + def test_find_setup_file_names_the_candidates(self, adapter, tmp_path): with pytest.raises(FileNotFoundError): - adapter.build_run_command(case_dir, 1, 1, selection) - assert not list(case_dir.iterdir()) + adapter.find_setup_file(tmp_path) + (tmp_path / "a.export").write_text("", encoding="utf-8") + (tmp_path / "b.export").write_text("", encoding="utf-8") + with pytest.raises(ValueError, match=r"a\.export, b\.export"): + adapter.find_setup_file(tmp_path) - def test_detect_outcome_done(self, adapter, tmp_path): + def test_generic_launch_in_every_runtime(self, adapter, tmp_path, monkeypatch): + monkeypatch.delenv("DISPLAY", raising=False) + case_dir = tmp_path / "RUNS" / "case1" + case_dir.mkdir(parents=True) + native = adapter.build_run_command( + case_dir, 1, 1, RuntimeSelection("native", "img", saturne_bin="/x/run_aster") + ) + assert native == ["nohup", "/x/run_aster", ".csauto.export"] + apptainer = adapter.build_run_command( + case_dir, + 1, + 1, + RuntimeSelection("singularity", "img", singularity_bin="apptainer", singularity_image="a.sif"), + ) + assert apptainer[-6:] == [ + "a.sif", + "bash", + "-c", + '[ ! -f /opt/activate.sh ] || source /opt/activate.sh && exec "$0" "$@"', + "run_aster", + ".csauto.export", + ] + + def test_detect_outcome_reads_the_diagnostic_on_stdout(self, adapter, tmp_path): case_dir = tmp_path / "case1" - logpath = case_dir / "RESU/LOGS" - logpath.mkdir(parents=True, exist_ok=True) - (logpath / "run_solver.log").write_text("DIAGNOSTIC JOB : OK\n", encoding="utf-8") + case_dir.mkdir() + stdout = case_dir / "csauto.stdout" + stdout.write_text("--- DIAGNOSTIC JOB : OK\n", encoding="utf-8") assert adapter.detect_outcome(case_dir) == STATUS_DONE - - def test_detect_outcome_failed(self, adapter, tmp_path): - case_dir = tmp_path / "case1" - logpath = case_dir / "RESU/LOGS" - logpath.mkdir(parents=True, exist_ok=True) - (logpath / "run_solver.log").write_text("DIAGNOSTIC JOB : _ABNORMAL_ABORT\n", encoding="utf-8") - assert adapter.detect_outcome(case_dir) == STATUS_FAILED + stdout.write_text("--- DIAGNOSTIC JOB : _ALARM\n", encoding="utf-8") + assert adapter.detect_outcome(case_dir) == STATUS_DONE + for verdict in ("_ABNORMAL_ABORT", "_ERROR", "_CPU_LIMIT", "NOOK_TEST_RESU", "?"): + stdout.write_text(f"--- DIAGNOSTIC JOB : {verdict}\n", encoding="utf-8") + assert adapter.detect_outcome(case_dir) == STATUS_FAILED, verdict + stdout.write_text("still running\n", encoding="utf-8") + assert adapter.detect_outcome(case_dir) is None def test_detect_outcome_ignores_logs_from_previous_runs(self, adapter, tmp_path): case_dir = tmp_path / "case1" - logpath = case_dir / "RESU/LOGS" - logpath.mkdir(parents=True, exist_ok=True) - log_file = logpath / "run_solver.log" - log_file.write_text("DIAGNOSTIC JOB : OK\n", encoding="utf-8") - relaunch = datetime.fromtimestamp(log_file.stat().st_mtime + 60) + case_dir.mkdir() + stdout = case_dir / "csauto.stdout" + stdout.write_text("--- DIAGNOSTIC JOB : OK\n", encoding="utf-8") + relaunch = datetime.fromtimestamp(stdout.stat().st_mtime + 60) assert adapter.detect_outcome(case_dir, start_time=relaunch.isoformat()) is None + def test_results_folder_is_the_single_run(self, adapter, tmp_path): + case_dir = tmp_path / "case1" + case_dir.mkdir() + assert adapter.list_run_dirs(case_dir) == [] + (case_dir / "RESU").mkdir() + assert adapter.list_run_dirs(case_dir) == [case_dir / "RESU"] + + def test_export_compare_kind_resolves_to_the_case_export(self, adapter, tmp_path): + (tmp_path / "study.export").write_text("", encoding="utf-8") + assert adapter.locate_case_file(tmp_path, "export") == tmp_path / "study.export" + class TestStubAdapter: @pytest.fixture() @@ -237,9 +240,9 @@ def _write_stub_log(self, case_dir, lines): return run_dir def test_run_argv_shape(self, adapter): - argv = adapter.run_argv("/runs/case1", 1, 5) + argv = adapter.run_argv(Path("/runs/case1"), 1, 5) assert argv[0] == "-c" - assert argv[-2:] == ["/runs/case1", "5"] + assert len(argv) == 2 # the script reads its steps from stub.toml, in the case folder def test_find_setup_file(self, adapter, tmp_path): with pytest.raises(FileNotFoundError): @@ -306,8 +309,8 @@ def test_code_aster_declares_doe_row_compare_kind() -> None: from csauto.solvers.code_aster import CodeAsterAdapter adapter = CodeAsterAdapter() - assert adapter.compare_kinds == (CompareKind("doe_row.csv", "doe_row.csv"),) - assert adapter.default_compare_kind == "doe_row.csv" + assert adapter.compare_kinds == (CompareKind("export", "Export file"), CompareKind("doe_row.csv", "doe_row.csv")) + assert adapter.default_compare_kind == "export" def test_code_saturne_exposes_every_capability_and_panel() -> None: @@ -339,40 +342,23 @@ def test_stub_capabilities_cover_compare_and_control() -> None: assert adapter.performance_fields == () -def test_always_on_panels_are_present_for_every_adapter() -> None: - from csauto.solvers import available_solvers, get_solver_adapter - from csauto.solvers.base import ALWAYS_ON_PANELS - - for name in available_solvers(): - panels = get_solver_adapter(name).dashboard_panels - assert set(ALWAYS_ON_PANELS) <= set(panels), name - - -def test_dashboard_panels_follow_declaration_order() -> None: - """Panels are ordered by ALL_DASHBOARD_PANELS, not by capability name.""" - from csauto.solvers.base import ALL_DASHBOARD_PANELS +def test_adapter_may_leave_out_a_panel_it_could_feed() -> None: from csauto.solvers.stub import StubAdapter - panels = StubAdapter().dashboard_panels - assert list(panels) == [p for p in ALL_DASHBOARD_PANELS if p in panels] - - -def test_adapter_cannot_declare_dashboard_panels() -> None: - """A class attribute would shadow the derived property and silently reintroduce the bug.""" - from csauto.solvers.base import SolverAdapterBase + class QuietStub(StubAdapter): + dashboard_panels = ("status", "tail") - with pytest.raises(TypeError, match="must not declare 'dashboard_panels'"): - - class BadAdapter(SolverAdapterBase): - dashboard_panels = ("status",) + adapter = QuietStub() + assert "compare" in adapter.capabilities + assert adapter.dashboard_panels == ("status", "tail") def test_adapter_cannot_declare_capabilities() -> None: - from csauto.solvers.base import SolverAdapterBase + from csauto.solvers.base import SolverAdapter with pytest.raises(TypeError, match="must not declare 'capabilities'"): - class BadAdapter(SolverAdapterBase): + class BadAdapter(SolverAdapter): capabilities = frozenset({"residuals"}) @@ -446,3 +432,87 @@ def test_code_saturne_detect_outcome_ignores_a_stale_failure_marker(tmp_path) -> start_time = datetime.fromtimestamp(time.time() - 60).isoformat(timespec="seconds") assert adapter.detect_outcome(case_dir, start_time) == STATUS_DONE + + +def test_code_saturne_finds_its_input_files(tmp_path) -> None: + adapter = get_solver_adapter("code_saturne") + with pytest.raises(FileNotFoundError): + adapter.find_setup_file(tmp_path) + assert adapter.find_run_config(tmp_path) is None + + (tmp_path / "DATA").mkdir() + (tmp_path / "DATA" / "setup.xml").write_text("", encoding="utf-8") + (tmp_path / "DATA" / "run.cfg").write_text("", encoding="utf-8") + assert adapter.find_setup_file(tmp_path) == tmp_path / "DATA" / "setup.xml" + assert adapter.find_run_config(tmp_path) == tmp_path / "DATA" / "run.cfg" + + nested = tmp_path / "other" + for sub in ("a", "b"): + (nested / sub).mkdir(parents=True) + (nested / sub / "setup.xml").write_text("", encoding="utf-8") + with pytest.raises(ValueError, match=r"Multiple setup\.xml"): + adapter.find_setup_file(nested) + + +def test_code_saturne_locates_run_files_newest_first(tmp_path) -> None: + adapter = get_solver_adapter("code_saturne") + case_dir = tmp_path / "case0001" + old_run = case_dir / "RESU" / "old" + new_run = case_dir / "RESU" / "new" + for run_dir in (old_run, new_run): + run_dir.mkdir(parents=True) + (old_run / "performance.log").write_text("old", encoding="utf-8") + (new_run / "listing").write_text("new", encoding="utf-8") + os.utime(old_run, (1, 1)) + (case_dir / "setup.xml").write_text("", encoding="utf-8") + + assert adapter.locate_case_file(case_dir, "listing") == new_run / "listing" + assert adapter.locate_case_file(case_dir, "performance.log") == old_run / "performance.log" + assert adapter.find_performance_log(case_dir) == old_run / "performance.log" + assert adapter.locate_case_file(case_dir, "setup.xml") == case_dir / "setup.xml" + assert adapter.locate_case_file(case_dir, "setup.log") is None + + +def test_anomaly_labels_are_checked_when_the_adapter_is_defined() -> None: + import re + + with pytest.raises(TypeError, match="unknown label 'fatal'"): + + class BadLabels(StubAdapter): + anomaly_patterns = (("fatal", re.compile("boom")),) + + +def test_derived_attributes_cannot_be_declared() -> None: + for name in ("capabilities", "performance_fields", "default_compare_kind"): + with pytest.raises(TypeError, match=name): + type("Bad", (StubAdapter,), {name: ()}) + + +def test_log_parsed_residuals_earn_the_capability() -> None: + class LogResiduals(StubAdapter): + def parse_live_residuals(self, case_dir): + return ["iteration", "r"], [{"iteration": "1", "r": "0.1"}] + + assert "residuals" in LogResiduals().capabilities + + +def test_resolve_runtime_respects_supported_runtimes(monkeypatch) -> None: + from csauto.execution import resolve_runtime + + stub = get_solver_adapter("stub") + with pytest.raises(ValueError, match="does not support the docker runtime"): + resolve_runtime("docker", adapter=stub) + # Auto never picks docker for a native-only solver, even when docker is installed. + monkeypatch.setattr("csauto.execution.shutil.which", lambda name: f"/usr/bin/{name}") + assert resolve_runtime("auto", adapter=stub).runtime == "native" + + +def test_docker_image_defaults_to_the_solvers_own(monkeypatch) -> None: + from csauto.execution import resolve_runtime + + monkeypatch.setattr("csauto.execution.shutil.which", lambda name: "/usr/bin/docker" if name == "docker" else None) + selection = resolve_runtime("docker", adapter=get_solver_adapter("code_aster")) + assert selection.docker_image == "simvia/code_aster:17.4.0" + assert ( + resolve_runtime("docker", docker_image="mine", adapter=get_solver_adapter("code_aster")).docker_image == "mine" + ) diff --git a/tests/unit/test_telemetry.py b/tests/unit/test_telemetry.py index f443a7e..4447636 100644 --- a/tests/unit/test_telemetry.py +++ b/tests/unit/test_telemetry.py @@ -13,3 +13,41 @@ def test_is_enabled_defaults_true_without_state_or_env(monkeypatch, tmp_path) -> monkeypatch.setattr(telemetry, "TELEMETRY_FILE", tmp_path / "telemetry.json") monkeypatch.delenv("CSAUTO_TELEMETRY_DISABLED", raising=False) assert telemetry.is_enabled() is True + + +def test_serve_ping_reports_the_solver_with_the_runtime(monkeypatch, tmp_path) -> None: + from types import SimpleNamespace + + from csauto.config import Config + from csauto.serve_commands import dispatch_serve_command + + sent: list[dict] = [] + monkeypatch.setattr(telemetry, "send_event", lambda event_type, **extra: sent.append(extra)) + monkeypatch.setattr(telemetry, "is_enabled", lambda: True) + args = SimpleNamespace(command="serve", no_doctor=True, api_token=None, host="127.0.0.1", port=0, runs_dir=tmp_path) + dispatch_serve_command( + args, + Config(solver="code_aster", runtime="docker"), + run_doctor=lambda *a, **k: [], + print_doctor=lambda items: False, + serve_fastapi=lambda *a, **k: None, + ) + assert [extra["id_docker"] for extra in sent] == ["code_aster:docker"] + + +def test_serve_announces_no_ping_when_telemetry_is_off(monkeypatch, tmp_path, capsys) -> None: + from types import SimpleNamespace + + from csauto.config import Config + from csauto.serve_commands import dispatch_serve_command + + monkeypatch.setenv("CSAUTO_TELEMETRY_DISABLED", "1") + args = SimpleNamespace(command="serve", no_doctor=True, api_token=None, host="127.0.0.1", port=0, runs_dir=tmp_path) + dispatch_serve_command( + args, + Config(), + run_doctor=lambda *a, **k: [], + print_doctor=lambda items: False, + serve_fastapi=lambda *a, **k: None, + ) + assert "[telemetry]" not in capsys.readouterr().out diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py index 02a5238..d1cd275 100644 --- a/tests/unit/test_template.py +++ b/tests/unit/test_template.py @@ -39,3 +39,13 @@ def test_render_template_preserves_shell_expansions() -> None: rendered = render_template(text, row, "case0004") assert "echo ${HOME}" in rendered assert "A" in rendered + + +def test_escaped_placeholder_is_kept_literally() -> None: + text = "print(f'step \\{i}')\nv = {velocity}\n" + assert extract_placeholders(text) == {"velocity"} + assert render_template(text, {"velocity": "2"}, "case0001") == "print(f'step {i}')\nv = 2\n" + + +def test_format_specs_and_shell_variables_are_not_placeholders() -> None: + assert extract_placeholders("f'{x:.3e}' ${HOME} {'a': 1}") == set() diff --git a/tests/web/test_adapter_api.py b/tests/web/test_adapter_api.py new file mode 100644 index 0000000..42d2f56 --- /dev/null +++ b/tests/web/test_adapter_api.py @@ -0,0 +1,205 @@ +"""HTTP API behaviour that comes from the solver adapter rather than from code_saturne.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from csauto.registry import STATUS_DONE, STATUS_FAILED, STATUS_PREPARED, STATUS_RUNNING, load_registry, save_registry +from csauto.solvers.base import ControlAction, PerfColumn, RestartMode +from csauto.solvers.stub import StubAdapter + +pytest.importorskip("fastapi") +pytest.importorskip("starlette") + + +class ProbeAdapter(StubAdapter): + """A stub with custom timing columns, a float control action and two restart modes.""" + + performance_columns = (PerfColumn("cpu_time", "CPU (s)", "time"), PerfColumn("peak_mb", "Peak (MB)", "int")) + control_actions = (ControlAction("stop", "Stop"), ControlAction("relax", "Relax", "Factor", "float")) + restart_modes = (RestartMode("again", "Run again"), RestartMode("more", "More steps", "Steps")) + anomaly_patterns = (("warn", re.compile(r"\bslow\b")),) + + def find_performance_log(self, case_dir): + return case_dir / "perf.txt" + + def parse_performance(self, path): + return {"cpu_time": "11.0", "peak_mb": "512"} + + def build_restart_args(self, case_dir, restart_mode, restart_value, restart_path): + return [], {} + + +@pytest.fixture() +def client_for(monkeypatch, runs_dir: Path): + from fastapi.testclient import TestClient + + from csauto.fastapi_app import create_fastapi_app + + def make(adapter): + monkeypatch.setattr("csauto.fastapi_app.get_solver_adapter", lambda _name=None: adapter) + return TestClient(create_fastapi_app(runs_dir)) + + return make + + +@pytest.fixture() +def stub_case(runs_dir: Path) -> Path: + case_dir = runs_dir / "case0001" + (case_dir / "OUT" / "run_0001").mkdir(parents=True) + (case_dir / "stub.toml").write_text("steps = 1\n", encoding="utf-8") + (case_dir / "perf.txt").write_text("", encoding="utf-8") + save_registry(runs_dir, {"case0001": {"case_id": "case0001", "path": str(case_dir), "status": STATUS_DONE}}) + return case_dir + + +def test_perf_returns_the_adapters_own_columns(client_for, stub_case) -> None: + data = client_for(ProbeAdapter()).get("/api/perf?case=case0001").json() + assert [c["key"] for c in data["columns"]] == ["cpu_time", "peak_mb"] + assert data["records"] == [{"case_id": "case0001", "cpu_time": "11.0", "peak_mb": "512"}] + + +def test_tail_files_always_offer_the_launcher_logs(client_for, stub_case) -> None: + (stub_case / "csauto.stdout").write_text("hello\nrun is slow\nerror: boom\n", encoding="utf-8") + client = client_for(ProbeAdapter()) + files = client.get("/api/tail_files?case=case0001").json()["files"] + assert "csauto.stdout" in files + lines = client.get("/api/tail_lines?case=case0001&file=csauto.stdout&n=10").json()["lines"] + assert [line["severity"] for line in lines] == [None, "warn", "error"] + + +def test_solver_images(client_for, stub_case) -> None: + from csauto.solvers import get_solver_adapter + + assert client_for(ProbeAdapter()).get("/api/solver_logo").status_code == 404 + response = client_for(get_solver_adapter("code_saturne")).get("/api/solver_logo") + assert response.status_code == 200 + assert response.headers["content-type"].startswith("image/svg+xml") + assert response.headers["cache-control"] == "no-cache" + + +def test_control_values_follow_the_declared_kind(client_for, stub_case, monkeypatch) -> None: + calls: list[tuple[str, object]] = [] + monkeypatch.setattr( + "csauto.fastapi_routes.actions.control_case", + lambda _runs, _case, action, **kwargs: calls.append((action, kwargs["value"])) or {}, + ) + client = client_for(ProbeAdapter()) + + def post(action: str, value: object = None) -> int: + return client.post( + "/api/control_case", json={"cases": ["case0001"], "action": action, "value": value} + ).status_code + + assert post("relax", 0.5) == 200 + assert post("stop") == 200 + assert post("stop", 2) == 400 # takes no value + assert post("relax") == 400 # needs one + assert post("pause") == 400 # not declared + assert calls == [("relax", 0.5), ("stop", None)] + + +def test_restart_modes_come_from_the_adapter(client_for, stub_case, monkeypatch) -> None: + calls: list[dict[str, object]] = [] + monkeypatch.setattr("csauto.fastapi_routes.actions.run_cases", lambda *_a, **kwargs: calls.append(kwargs)) + monkeypatch.setattr("csauto.fastapi_routes.actions.resolve_runtime", lambda **_kw: _Selection()) + client = client_for(ProbeAdapter()) + + def run(**restart: object) -> int: + payload = {"cases": ["case0001"], "n": 1, "nt": 1, "restart": True, **restart} + return client.post("/api/run_case", json=payload).status_code + + assert run(restart_mode="more", restart_value=10, restart_path="run_0001") == 200 + assert run(restart_mode="more", restart_value=2.5) == 400 # whole number expected + assert run(restart_mode="again") == 200 + assert run(restart_mode="iterations", restart_value=10) == 400 # code_saturne's mode, not this solver's + assert [(c["restart_mode"], c["restart_value"], c["restart_path"]) for c in calls] == [ + ("more", 10, "run_0001"), + ("again", None, None), + ] + + +class _Selection: + runtime = "native" + docker_image = "" + saturne_bin = "/usr/bin/python3" + singularity_image = None + singularity_bin = None + + +def test_clean_resets_a_case_only_when_its_runs_are_deleted(client_for, stub_case, runs_dir: Path) -> None: + registry = load_registry(runs_dir) + registry["case0001"]["start_time"] = "2026-01-01T00:00:00" + save_registry(runs_dir, registry) + client = client_for(StubAdapter()) + logs_only = {"cases": ["case0001"], "prune_resu": False, "max_log_mb": 50} + assert client.post("/api/cleanup_cases", json=logs_only).status_code == 200 + assert load_registry(runs_dir)["case0001"]["status"] == STATUS_DONE + + prune = {"cases": ["case0001"], "prune_resu": True, "keep_last": 0} + assert client.post("/api/cleanup_cases", json=prune).json()["resu_removed"] == 1 + assert load_registry(runs_dir)["case0001"]["status"] == STATUS_PREPARED + + class VerdictOutsideRuns(StubAdapter): # code_aster reads its verdict from csauto.stdout, which Clean keeps + def detect_outcome(self, case_dir, start_time=None): + return STATUS_DONE + + from csauto.runner import refresh_status + + refresh_status(runs_dir, adapter=VerdictOutsideRuns()) + assert load_registry(runs_dir)["case0001"]["status"] == STATUS_PREPARED + + +def test_clean_keeps_the_status_of_cases_it_did_not_touch(client_for, stub_case, runs_dir: Path) -> None: + (runs_dir / "case0002").mkdir() # failed before writing any run folder + (runs_dir / "case0003" / "OUT" / "run_0001").mkdir(parents=True) + registry = load_registry(runs_dir) + registry["case0002"] = {"case_id": "case0002", "status": STATUS_FAILED} + registry["case0003"] = {"case_id": "case0003", "status": STATUS_RUNNING} + save_registry(runs_dir, registry) + + cases = {"cases": ["case0002", "case0003"], "prune_resu": True, "keep_last": 0} + report = client_for(StubAdapter()).post("/api/cleanup_cases", json=cases).json() + + assert report["skipped_active"] == ["case0003"] + assert (runs_dir / "case0003" / "OUT" / "run_0001").is_dir() + assert load_registry(runs_dir)["case0002"]["status"] == STATUS_FAILED + + +def test_file_routes_never_leave_the_case_folder(client_for, stub_case, tmp_path: Path) -> None: + secret = tmp_path / "secret.txt" + secret.write_text("token\n", encoding="utf-8") + + class LeakyAdapter(StubAdapter): + def locate_case_file(self, case_dir, name): + return secret + + client = client_for(LeakyAdapter()) + for url in ("/api/tail?case=case0001&file=x", "/api/tail_lines?case=case0001&file=x"): + assert client.get(url).status_code == 404, url + assert client.get("/api/case_file?case=case0001&kind=x").status_code == 404 + update = {"case": "case0001", "kind": "x", "content": "overwritten"} + assert client.post("/api/case_file", json=update).status_code == 404 + assert secret.read_text(encoding="utf-8") == "token\n" + + stub = client_for(StubAdapter()) + for name in ("../../secret.txt", str(secret), "bad%00name"): + assert stub.get(f"/api/tail_lines?case=case0001&file={name}").status_code == 404, name + + +def test_kill_only_searches_docker_for_docker_runs(runs_dir: Path, monkeypatch) -> None: + from csauto.web_support import kill_case + + lookups: list[tuple[str, Path]] = [] + monkeypatch.setattr( + "csauto.web_support.find_container_id_for_case", + lambda case_id, runs: lookups.append((case_id, runs)) or None, + ) + for runtime in ("native", "docker"): + save_registry(runs_dir, {"case0001": {"case_id": "case0001", "status": "RUNNING", "runtime": runtime}}) + with pytest.raises(ValueError, match="No PID, container_id or job_id"): + kill_case(runs_dir, "case0001", actor=None, job_id_patterns=[], adapter=StubAdapter()) + assert lookups == [("case0001", runs_dir)] diff --git a/tests/web/test_api.py b/tests/web/test_api.py index 8cbc934..36a6c0d 100644 --- a/tests/web/test_api.py +++ b/tests/web/test_api.py @@ -300,11 +300,19 @@ def test_api_probes_and_residuals_svg(web_env) -> None: data = json.loads(body) assert "probe_density.csv" in data["files"] + status, body = _http_get(f"{base_url}/api/probes?case=case0001&scope=probes") + assert status == 200 + assert json.loads(body)["files"] == data["files"] + status, body = _http_get(f"{base_url}/api/probes?case=case0001&scope=profiles") assert status == 200 profiles = json.loads(body)["files"] assert "profiles/profile_temperature.csv" in profiles + with pytest.raises(HTTPError) as excinfo: + _http_get(f"{base_url}/api/probes?case=case0001&scope=monitoring") + assert excinfo.value.code == 400 + status, body = _http_get(f"{base_url}/api/probe_columns?case=case0001&probe=probe_density.csv") assert status == 200 cols = json.loads(body)["columns"] @@ -647,7 +655,7 @@ def test_api_run_case_without_max_parallel_uses_running_plus_requested( } save_registry(runs_dir, registry) - monkeypatch.setattr("csauto.web_support.is_process_alive", lambda pid: int(pid) == 4242) + monkeypatch.setattr("csauto.web_support.is_process_alive", lambda pid, **_kw: int(pid) == 4242) monkeypatch.setattr( "csauto.fastapi_routes.actions.resolve_runtime", lambda **_kwargs: RuntimeSelection(runtime="docker", docker_image="dummy/image:latest"), @@ -1228,6 +1236,7 @@ def test_api_app_config_exposes_solver_and_panels(web_env) -> None: ] assert data["compare_kinds"][0]["label"] == "setup.xml" assert data["error_files"] == ["csauto.stderr", "run_solver.log", "listing", "csauto.stdout"] + assert data["tail_files"][0] == "run_solver.log" assert data["capabilities"] == [ "compare", "control", @@ -1237,7 +1246,16 @@ def test_api_app_config_exposes_solver_and_panels(web_env) -> None: "residuals", "restart", ] - assert data["control_actions"] == ["checkpoint", "extend", "flush", "stop"] + assert [a["name"] for a in data["control_actions"]] == ["stop", "extend", "checkpoint", "flush"] + assert data["control_actions"][1] == { + "name": "extend", + "label": "Extend", + "value_label": "Additional time steps", + "value_kind": "int", + } + assert [m["name"] for m in data["restart_modes"]] == ["iterations", "physical_time"] + assert data["default_residual_columns"] == ["velocity", "pressure"] + assert data["logo"] is True and data["icon"] is True def test_api_app_config_reflects_a_solver_without_analytics( @@ -1268,6 +1286,26 @@ def test_api_app_config_reflects_a_solver_without_analytics( _ACTIVE_TEST_CLIENT = None +def test_api_tail_defaults_to_the_solver_log(runs_dir: Path, case_factory, registry_factory) -> None: + global _ACTIVE_TEST_CLIENT + + case_dir = case_factory(runs_dir, "case0001") + run_dir = case_dir / "OUT" / "run_0001" + run_dir.mkdir(parents=True) + (run_dir / "stub.log").write_text("step 1\nstep 2\n", encoding="utf-8") + registry_factory(runs_dir, "case0001", case_dir, status="DONE") + base_url, thread, httpd = _start_server(runs_dir, solver="stub") + try: + status, body = _http_get(f"{base_url}/api/tail?case=case0001") + assert status == 200 + assert "step 2" in body + finally: + httpd.shutdown() + httpd.server_close() + thread.join(timeout=2) + _ACTIVE_TEST_CLIENT = None + + @pytest.fixture() def aster_env(runs_dir: Path, case_factory, registry_factory): """A server backed by a solver with no control, restart or GUI capability.""" diff --git a/tests/web/test_fastapi_server.py b/tests/web/test_fastapi_server.py index 63bafb8..7f34455 100644 --- a/tests/web/test_fastapi_server.py +++ b/tests/web/test_fastapi_server.py @@ -242,7 +242,7 @@ def test_fastapi_server_run_case_validation_error(fastapi_server_env) -> None: ) assert excinfo.value.code == 400 payload = json.loads(excinfo.value.read().decode("utf-8")) - assert payload["detail"] == "restart_value must be a positive integer for iterations" + assert payload["detail"] == "iterations needs a positive value (Iterations)" def test_fastapi_server_case_convergence_and_compare(fastapi_server_env) -> None: