JobsPage

tcmts.studio.JobsPage · #wtt3zdfi · term · runs on the server

is an async component

The app's background jobs (as tcm jobs lists them): how many are queued, running, done, failed and cancelled, each state a filter (status); the newest 200, each with its function, its attempts, when it runs or ran, and its error; one (job) in full: its arguments, its error, its trace, with Retry or Cancel. Beside them, the app's recurring jobs (every()), each with how often and its last run. Made again every few seconds while it's shown.

import { ago, at, codeHref, servedCodebase, shareNow } from "#codebase/tcmts/share";
import { JobButtons, jobList, Live, scheduleList, StudioFrame, studioShare } from "#codebase/tcmts/studio";

export async function JobsPage({ status, job }: { status: string; job: string }) {
  studioShare();
  const s = shareNow();
  if (!s.signedIn)
    return (
      <StudioFrame tab="jobs" title="Sign in">
        {null}
      </StudioFrame>
    );
  const cb = servedCodebase().cb;
  const own = s.cb === cb;
  const { jobs, counts } = jobList(cb, status);
  // (the recurring jobs the code has, found again only when it changes; their last runs, as the queue has them now)
  const found = s.cache.atHead(cb, "studio schedules", () =>
    scheduleList(cb).map(({ name, spec }) => ({ name, ...(spec ? { spec } : {}) })),
  );
  const schedules = scheduleList(cb, found);
  const shown = job
    ? (jobs.find((j) => j.id === job) ?? jobList(cb, "").jobs.find((j) => j.id === job))
    : undefined;
  const href = (p: { status?: string; job?: string }) => {
    const u = new URLSearchParams();
    const st = p.status ?? status;
    if (st) u.set("status", st);
    if (p.job) u.set("job", p.job);
    return at(`/jobs${u.size ? `?${u}` : ""}`);
  };
  const named = (hash: string, name: string) =>
    cb.store.pathsOf(hash.replace(/#fn$/, ""))[0] ?? name;
  const when = (j: { status: string; runAt: number; finished?: number }) =>
    j.status === "queued"
      ? j.runAt > Date.now()
        ? `runs in ${ago(2 * Date.now() - j.runAt).replace(/ ago$/, "")}`
        : "due"
      : ago(j.finished ?? j.runAt);
  const total = Object.values(counts).reduce((n, x) => n + x, 0);
  const STATES = ["queued", "running", "failed", "done", "cancelled"];
  return (
    <StudioFrame tab="jobs" title="Jobs">
      <Live every={3000} />
      <div className="pagehead">
        <h2 className="title">Jobs</h2>
        <span className="muted">
          the app's background jobs, as tcm dev and tcm start run them on this machine
        </span>
        <span className="right seg">
          <a
            className="btn small"
            href={href({ status: "" })}
            {...(status ? {} : { "data-selected": "" })}
          >
            {`all ${total}`}
          </a>
          {STATES.filter((x) => counts[x]).map((x) => (
            <a
              key={x}
              className="btn small"
              href={href({ status: x })}
              {...(status === x ? { "data-selected": "" } : {})}
            >
              <span className={x === "failed" ? "bad" : ""}>{`${x} ${counts[x]}`}</span>
            </a>
          ))}
        </span>
      </div>
      <div className="cols-2" style={{ display: "grid", gap: 24 }}>
        <div>
          {shown && (
            <div className="box">
              <div className="boxhead">
                <b className="mono">{named(shown.hash, shown.name)}</b>
                <span
                  className={`muted${shown.status === "failed" ? " bad" : ""}`}
                >{`${shown.status}, ${when(shown)}`}</span>
                <a className="right" href={href({})}>
                  Close
                </a>
              </div>
              <div className="boxbody errbody">
                <dl className="kv">
                  <dt>id</dt>
                  <dd className="mono">{shown.id}</dd>
                  <dt>attempts</dt>
                  <dd>{`${shown.attempts} of ${shown.maxAttempts}`}</dd>
                  <dt>queued</dt>
                  <dd>{ago(shown.created)}</dd>
                  {shown.slot && (
                    <>
                      <dt>schedule</dt>
                      <dd className="mono">{shown.slot.split("@")[0]}</dd>
                    </>
                  )}
                  {shown.trace && (
                    <>
                      <dt>trace</dt>
                      <dd>
                        <a
                          className="mono"
                          href={at(`/trace/${shown.trace}`)}
                        >{`trace ${shown.trace.slice(0, 8)}`}</a>
                      </dd>
                    </>
                  )}
                </dl>
                <div>
                  <div className="muted" style={{ fontSize: 12.5, marginBottom: 4 }}>
                    its arguments
                  </div>
                  <pre className="code">{shown.args}</pre>
                </div>
                {shown.error && <pre className="code errmsg">{shown.error}</pre>}
                {own && <JobButtons id={shown.id} status={shown.status} />}
              </div>
            </div>
          )}
          <div className="box">
            {jobs.length ? (
              jobs.map((j) => (
                <a
                  className={`lrow tracerow${j.id === job ? " on" : ""}`}
                  key={j.id}
                  href={href({ job: j.id })}
                >
                  <div>
                    <span className={`jobstate ${j.status}`}>{j.status}</span>{" "}
                    <span className="mono">{named(j.hash, j.name)}</span>{" "}
                    {j.error && j.status !== "done" && (
                      <span className="bad jobwhy">{j.error.split("\n")[0]}</span>
                    )}
                  </div>
                  <span className="muted">{`${j.attempts}/${j.maxAttempts} · ${when(j)}`}</span>
                </a>
              ))
            ) : (
              <div className="empty">
                {status
                  ? `No job is ${status}.`
                  : "No jobs yet: enqueue() from a server function queues one, and every() runs one again and again."}
              </div>
            )}
          </div>
        </div>
        <aside className="side">
          <div className="sect">
            <h4>Recurring</h4>
            {schedules.length ? (
              schedules.map((x) => (
                <div key={x.name} style={{ marginBottom: 8 }}>
                  <a className="mono" href={codeHref(x.name)}>
                    {x.name}
                  </a>
                  <span className="muted" style={{ display: "block", fontSize: 12.5 }}>
                    {[
                      x.spec && `every ${x.spec}`,
                      x.last
                        ? `last ${x.last.status} ${ago(x.last.finished ?? x.last.runAt)}`
                        : x.firstSeen
                          ? "not run yet"
                          : "not seen by the app yet",
                    ]
                      .filter(Boolean)
                      .join(" · ")}
                  </span>
                </div>
              ))
            ) : (
              <span className="muted">None: every() makes one.</span>
            )}
          </div>
        </aside>
      </div>
    </StudioFrame>
  );
}

uses

used by