Zum Inhalt

touchstone/gui/static/live_progress.js

// live_progress.js — native-EventSource live progress for a running run card.
//
// The htmx SSE extension is NOT bundled (htmx core dropped SSE in 2.0), so `hx-ext="sse"`
// was a no-op and the card sat at 0/0. We consume the server's `view` SSE events directly
// via the browser's native EventSource. Loaded NON-deferred so the Alpine component is
// registered before the (deferred) Alpine starts — same constraint as model_picker.js.
"use strict";

document.addEventListener("alpine:init", () => {
  Alpine.data("liveProgress", (name, kind) => ({
    name: name,
    kind: kind || "eval",
    done: 0,
    total: 0,
    pct: 0,
    ok: 0,
    failed: 0,
    preflight: [],
    finished: false,
    _es: null,
    _poll: null,
    start() {
      try {
        this._es = new EventSource(
          "/live/" + encodeURIComponent(this.name) + "?kind=" + encodeURIComponent(this.kind),
        );
      } catch (e) {
        return;
      }
      this._es.addEventListener("view", (ev) => {
        let d;
        try {
          d = JSON.parse(ev.data);
        } catch (e) {
          return;
        }
        this.total = d.total || 0;
        this.done = d.done || 0;
        this.ok = d.ok || 0;
        this.failed = d.failed || 0;
        // preflight is folded into the view by build_view (Task 8); show only non-ok models
        this.preflight = (d.preflight || []).filter((p) => p.status !== "ok");
        this.pct = this.total ? Math.round((this.done / this.total) * 100) : 0;
        if (d.finished && this._es) {
          this._es.close();
          this._es = null;
          this.finished = true;
          this._awaitFinalized();
        }
      });
    },
    _awaitFinalized() {
      // 'finished' fires at run_done — BEFORE the bundle is finalized and the sentinel marked
      // terminal. The status badge/card are server-rendered, so we reload the overview to
      // re-derive them — but only once the run is no longer active (condition, not a guessed
      // delay), else discover would still classify it 'running'.
      const tick = () => {
        fetch("/run-active/" + encodeURIComponent(this.name))
          .then((r) => r.json())
          .then((d) => {
            if (!d.active) {
              location.reload();
            } else {
              this._poll = setTimeout(tick, 800);
            }
          })
          .catch(() => location.reload()); // server gone/error: a reload is the safe fallback
      };
      tick();
    },
    destroy() {
      if (this._es) this._es.close();
      if (this._poll) clearTimeout(this._poll);
    },
  }));
});