rustango 0.61.13

Batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
Documentation
{% extends "op_layout.html" %}
{% block title %}{{ verb }} {{ slug }} — {{ brand_name | default(value='Rustango Operator Console') }}{% endblock %}
{% block content %}
<p class="breadcrumb"><a href="{{ console_prefix }}/orgs">Organizations</a><span class="sep">/</span><a href="{{ console_prefix }}/orgs/provision">Runs</a><span class="sep">/</span>{{ verb }} {{ slug }}</p>
<h1>{{ verb }} <code>{{ slug }}</code></h1>

<p class="lead">
  Run #{{ run_id }} — <strong id="run-state" data-state="{{ state }}">{{ state }}</strong>
  {% if not terminal %}<span id="live-note"> · following live</span>{% endif %}
</p>

{% if error %}
<div class="alert alert-error alert-lines">{{ error }}</div>
{% endif %}

{% if handover %}
<div class="alert alert-secret" id="first-admin">
  {% if handover.mailed_to %}
  A set-password link for <code>{{ handover.username }}</code> was mailed to
  <code>{{ handover.mailed_to }}</code>. It works once and expires in {{ handover.hours }} hours.
  {% elif handover.link %}
  {% if handover.mail_failed %}The mail could not be sent. {% endif %}
  Give this set-password link to <code>{{ handover.username }}</code>. It is shown
  only now, works once, and expires in {{ handover.hours }} hours.
  <div class="link-copy">
    <input type="text" id="handover-link" readonly value="{{ handover.link }}" aria-label="Set-password link"
           autocomplete="off">
    <button type="button" class="btn btn-secondary" id="copy-link">Copy</button>
  </div>
  <script nonce="{{ csp_nonce }}">
    document.getElementById("copy-link").addEventListener("click", async (e) => {
      const input = document.getElementById("handover-link");
      try {
        await navigator.clipboard.writeText(input.value);
      } catch (_) {
        // No clipboard API (plain HTTP): fall back to the selection.
        input.select();
        document.execCommand("copy");
      }
      e.target.textContent = "Copied";
    });
  </script>
  {% else %}
  The first admin <code>{{ handover.username }}</code> was created, but no link could
  be issued. Run <code>set-password-link {{ slug }} {{ handover.username }}</code>.
  {% endif %}
</div>
{% endif %}

<table>
  <thead>
    <tr><th>#</th><th>Step</th><th>Status</th><th>Detail</th></tr>
  </thead>
  <tbody id="events">
    {#- One row per step at its latest status; `data-step` lets the live
        stream update it in place. `info` rows are details, one per event. -#}
    {% for e in events %}
    <tr data-seq="{{ e.seq }}"{% if e.status != "info" %} data-step="{{ e.step }}"{% endif %}>
      <td>{{ loop.index }}</td>
      <td><code>{{ e.step }}</code></td>
      <td class="status-{{ e.status }}">{{ e.status }}</td>
      <td class="detail">{% for p in e.parts %}{% if p.code %}<code>{{ p.text }}</code>{% else %}{{ p.text }}{% endif %}{% endfor %}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

<p class="form-actions"><a href="{{ console_prefix }}/orgs" class="btn btn-secondary">Back to tenants</a></p>

{% if not terminal %}
<script nonce="{{ csp_nonce }}">
  // Resume from the last row already rendered, so a reload does not
  // replay what is on screen — and a reconnect mid-run picks up
  // exactly where it stopped. The server honours `Last-Event-ID` too;
  // this is the explicit form for the first connection.
  (function () {
    const tbody = document.getElementById("events");
    // A step row is updated in place, so the newest seq can sit anywhere.
    let last = 0;
    for (const r of tbody.querySelectorAll("tr[data-seq]")) {
      last = Math.max(last, Number(r.dataset.seq));
    }

    const es = new EventSource(consoleUrl("/orgs/provision/{{ run_id }}/stream?after=" + last));

    // `a `b` c` as text and <code> nodes. textContent, never innerHTML:
    // `message` carries driver text and hostnames that are not ours.
    function fillMessage(td, message) {
      const parts = String(message || "").split("`");
      td.replaceChildren();
      if (parts.length % 2 === 0) { td.textContent = message; return; }
      parts.forEach((p, i) => {
        if (!p) { return; }
        if (i % 2) {
          const c = document.createElement("code");
          c.textContent = p;
          td.appendChild(c);
        } else {
          td.append(p);
        }
      });
    }

    es.addEventListener("step", (msg) => {
      const e = JSON.parse(msg.data);
      let tr = e.status === "info"
        ? null
        : tbody.querySelector('tr[data-step="' + CSS.escape(e.step) + '"]');
      if (!tr) {
        tr = document.createElement("tr");
        if (e.status !== "info") { tr.dataset.step = e.step; }
        const n = document.createElement("td");
        n.textContent = tbody.rows.length + 1;
        const step = document.createElement("td");
        const code = document.createElement("code");
        code.textContent = e.step;
        step.appendChild(code);
        tr.append(n, step, document.createElement("td"), document.createElement("td"));
        tbody.appendChild(tr);
      }
      tr.dataset.seq = e.seq;
      const status = tr.cells[2];
      const wasFailed = status.textContent === "failed";
      status.className = "status-" + e.status;
      status.textContent = e.status;
      if (e.message || wasFailed) { fillMessage(tr.cells[3], e.message); }
    });

    es.addEventListener("done", () => {
      es.close();
      // Re-read rather than guess the final state: the stream says
      // "finished", the page says what it finished as.
      window.location.reload();
    });

    es.addEventListener("error", (msg) => {
      const note = document.getElementById("live-note");
      if (note && msg.data) { note.textContent = " · " + msg.data; }
    });
  })();
</script>
{% endif %}
{% endblock %}