Documentation

Mount the bundled assets and wire verified solverforge-ui primitives into an Axum app.

Getting Started with solverforge-ui

This guide covers the verified integration path:

  1. add the crate
  2. mount /sf/* assets with .merge(solverforge_ui::routes())
  3. include the bundled CSS and JS
  4. instantiate components plus the retained-job backend and solver helpers

Add the Dependency

[dependencies]
axum = "0.8.9"
solverforge-ui = { version = "0.9.0" }

# Pin a specific GitHub release tag when you need exact reproducibility.
# solverforge-ui = { git = "https://github.com/SolverForge/solverforge-ui", tag = "v0.9.0" }

Use the Git tag form when you need exact source-tag reproducibility instead of the crates.io package.

Use Rust 1.95+ for the current crate line.

Mount /sf/* Routes in Axum

use axum::{routing::get, Router};

async fn index() -> &'static str {
    include_str!("../static/index.html")
}

fn app() -> Router {
    Router::new()
        .route("/", get(index))
        .merge(solverforge_ui::routes())
}

solverforge_ui::routes() serves the embedded /sf/* assets. Your application still owns its HTML pages and any job/solver API routes.

Include Required Assets in HTML

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>solverforge-ui quickstart</title>

    <link
      rel="stylesheet"
      href="/sf/vendor/fontawesome/css/fontawesome.min.css"
    />
    <link rel="stylesheet" href="/sf/vendor/fontawesome/css/solid.min.css" />
    <link rel="stylesheet" href="/sf/sf.css" />
  </head>
  <body class="sf-app">
    <script src="/sf/sf.js"></script>
    <script>
      var tabs = SF.createTabs({
        tabs: [
          { id: "plan", content: { unsafeHtml: "<div>Plan view</div>" }, active: true },
          { id: "gantt", content: { unsafeHtml: "<div>Gantt view</div>" } },
        ],
      });
      document.body.appendChild(tabs.el);

      var backend = SF.createBackend({ type: "axum" });
      var solver;

      var header = SF.createHeader({
        title: "SolverForge Scheduler",
        subtitle: "by SolverForge",
        tabs: [
          { id: "plan", label: "Plan", active: true },
          { id: "gantt", label: "Gantt" },
        ],
        onTabChange: function (id) {
          tabs.show(id);
        },
        actions: {
          onSolve: function () {
            solver.start();
          },
          onPause: function () {
            solver.pause();
          },
          onResume: function () {
            solver.resume();
          },
          onCancel: function () {
            solver.cancel();
          },
        },
      });
      document.body.prepend(header);

      var statusBar = SF.createStatusBar({ header: header, constraints: [] });
      header.after(statusBar.el);

      solver = SF.createSolver({
        backend: backend,
        statusBar: statusBar,
        onProgress: function (meta) {
          console.log("progress", meta.currentScore, meta.bestScore);
        },
        onSolution: function (snapshot, meta) {
          renderAndSync(snapshot, meta);
        },
        onPaused: function (snapshot, meta) {
          renderAndSync(snapshot, meta);
        },
        onComplete: function (snapshot, meta) {
          renderAndSync(snapshot, meta);
        },
        onCancelled: function (snapshot, meta) {
          renderAndSync(snapshot, meta);
        },
        onFailure: function (message, meta, snapshot, analysis) {
          console.error(message, analysis);
          renderAndSync(snapshot, meta);
        },
      });

      function renderAndSync(snapshot, meta) {
        try {
          if (snapshot && snapshot.solution) {
            render(snapshot.solution);
          }
        } finally {
          syncMarkers(meta);
        }
      }

      function render(solution) {
        // Replace this with the app's domain-specific renderer.
        console.log("solution", solution);
      }

      function syncMarkers(meta) {
        document.body.dataset.jobId = (meta && meta.jobId) || "";
        document.body.dataset.snapshotRevision =
          meta && meta.snapshotRevision != null ? String(meta.snapshotRevision) : "";
        document.body.dataset.lifecycleState = (meta && meta.lifecycleState) || "IDLE";
      }
    </script>
  </body>
</html>

SF.createSolver(...) now follows the retained-job lifecycle. Use onProgress(...) for score and telemetry updates, onSolution(...) for snapshot-bearing best_solution events, and onPaused(...), onCancelled(...), or onComplete(...) when you need the exact retained snapshot after the runtime reaches an authoritative lifecycle state. Snapshot-bearing callbacks are null-safe: cancellation, failure, or a failed snapshot synchronization can leave no solution snapshot, so render only when snapshot && snapshot.solution exists and synchronize lifecycle markers in a finally block.

The header renders the cancel action as Stop. Keep using the onCancel configuration key and call solver.cancel(); delete() is reserved for terminal retained-job cleanup.

Application Routes

solverforge-ui does not generate your scheduling API. The crate ships the UI surface and a set of backend helpers. If you use SF.createBackend({ type: 'axum' }), follow the canonical retained-job contract documented in Integration & Assets. Older Schedule-named route shapes are historical. Current generated apps expose the retained /jobs/... contract.

Next Steps