@edgehero/pi-dispatch-admin 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/console.mjs +520 -398
  2. package/dist/index.mjs +512 -389
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -15037,8 +15037,9 @@ function pauseWindowsVersionRefusal(doc, { supported = PAUSE_WINDOWS_VERSION } =
15037
15037
  function heldPauseWindows(text4, path6) {
15038
15038
  if ((text4 ?? "").trim() === "") return { doc: null, windows: [] };
15039
15039
  const { value, problem } = heldDocument(text4, "windows");
15040
- const refused3 = problem ?? (findDuplicateKey(text4) ? "has a key written twice" : null);
15041
- if (refused3) return { doc: value ?? null, problem: `the pause-windows file ${path6} ${refused3}, so its windows cannot be kept` };
15040
+ const doubled = value !== void 0 && findDuplicateKey(text4) !== null;
15041
+ const refused3 = doubled ? "has a key written twice" : problem ?? null;
15042
+ if (refused3) return { doc: doubled ? null : value ?? null, problem: `the pause-windows file ${path6} ${refused3}, so its windows cannot be kept` };
15042
15043
  return { doc: value, windows: Array.isArray(value.windows) ? value.windows : [] };
15043
15044
  }
15044
15045
  function pauseWindowsText(doc, windows, { table: table3 = PAUSE_WINDOW_FIELD_VERSIONS, valueTable = PAUSE_WINDOW_VALUE_VERSIONS } = {}) {
@@ -15089,6 +15090,10 @@ function parsePauseWindows(text4, path6, { supported = PAUSE_WINDOWS_VERSION, ta
15089
15090
  } catch (error) {
15090
15091
  throw configError4(`pause-windows file is not valid JSON: ${path6} (${error.message})`);
15091
15092
  }
15093
+ const duplicate = findDuplicateKey(text4);
15094
+ if (duplicate) {
15095
+ throw configError4(`pause-windows file has a duplicate key ${JSON.stringify(duplicate.key)} at ${duplicate.at}: JSON keeps the LAST value, so the file a person reads and the file that applies are not the same: ${path6}`);
15096
+ }
15092
15097
  const problem = pauseWindowsVersionRefusal(parsed, { supported });
15093
15098
  if (problem) {
15094
15099
  const error = configError4(`${problem.message}: ${path6}`);
@@ -114901,10 +114906,12 @@ function withdrawCancel({ redis, jobId, interventionOrigin = "human" }) {
114901
114906
  Promise.resolve().then(() => redis.del(cancelRequestKeyOf(jobId, interventionOrigin))).catch(() => {
114902
114907
  });
114903
114908
  }
114904
- async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)) }) {
114909
+ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)), now = Date.now, onWritten = null }) {
114905
114910
  const requestKey = cancelRequestKeyOf(jobId, interventionOrigin);
114906
114911
  const ackKey = interventionOrigin === "agent" ? agentCancelAckKey(jobId) : cancelAckKey(jobId);
114907
114912
  await redis.set(requestKey, interventionOrigin === "agent" ? "agent-cancel" : "operator-cancel", "PX", CANCEL_REQ_TTL_MS);
114913
+ onWritten?.();
114914
+ const deadline = now() + ackTimeoutMs;
114908
114915
  for (let waited = 0; ; waited += pollMs) {
114909
114916
  let host;
114910
114917
  try {
@@ -114914,7 +114921,7 @@ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTi
114914
114921
  throw err;
114915
114922
  }
114916
114923
  if (host !== null && host !== void 0) return { ack: host };
114917
- if (waited + pollMs > ackTimeoutMs) break;
114924
+ if (waited + pollMs > ackTimeoutMs || now() >= deadline) break;
114918
114925
  await sleep5(pollMs);
114919
114926
  }
114920
114927
  try {
@@ -122801,7 +122808,7 @@ function triggerLastRun(records, hostLoads = null) {
122801
122808
  ...sourcesOf(loaded, "package").filter((row2) => row2.toolNames?.length > 0).map((row2) => ({ kind: "package", from: row2.name ?? "a staged package", names: [...row2.toolNames] })),
122802
122809
  ...(overlay?.namedTools ?? []).map((row2) => ({ kind: "overlay", from: row2.name, names: [...row2.tools] }))
122803
122810
  ];
122804
- const sources = [...sourcesOf(loaded, "package"), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
122811
+ const sources = [...sourcesOf(loaded, "package").map((row2) => ({ ...row2, kind: "package" })), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, kind: "overlay", tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
122805
122812
  return {
122806
122813
  newest,
122807
122814
  quoted: { id: runIdOf2(record3), host: facts7.host, record: record3, rows: Object.fromEntries(loadedRows(record3, { past: true }).map((row2) => [row2.label, row2])), problems, sources, tools, notes: [...cutWords(loaded), ...wentOn(record3) && loaded.at !== "ready" ? [LOADED_WORDS.notedEarly] : []] }
@@ -122829,12 +122836,12 @@ function hostLoadsRow(hostLoads, name2) {
122829
122836
  const row2 = Array.isArray(hostLoads?.hosts) ? hostLoads.hosts.find((entry3) => entry3?.name === name2) : null;
122830
122837
  return row2 ?? { name: name2, state: "unknown", why: hostLoads?.unreachable ? "unreachable" : "absent" };
122831
122838
  }
122832
- function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null }) {
122839
+ function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null, mcp = null }) {
122833
122840
  const row2 = hostLoadsRow(hostLoads, host);
122834
122841
  const first = noOverlay(hostPiFor(row2, image));
122835
122842
  const unknown3 = (why) => [`${first} What a job of trigger ${triggerIndex + 1} would load on this host is unknown: ${why}.`];
122836
122843
  if (row2.state === "unknown") return unknown3(row2.why === "unreachable" ? "the hosts\u2019 doctor runs could not be read" : row2.why === "unreadable" ? "its last doctor run could not be read" : "it has published no doctor run");
122837
- const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed));
122844
+ const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed) && (part !== "mcp" || !Array.isArray(mcp) || mcp.length > 0));
122838
122845
  if (unread2.length > 0) return unknown3(`its last doctor run could not read ${listed(unread2.map((part) => UNREAD_PART_WORDS[part] ?? "part of what it publishes"))}`);
122839
122846
  const packages = withholdsPackages ? [] : (row2.packages ?? []).filter((entry3) => !excludes.includes(entry3.name)).map((entry3) => `${entry3.name} ${entry3.version}`);
122840
122847
  const extensions = row2.overlayExtensionsAllowed ? (row2.extensions ?? []).map((entry3) => entry3.name).filter((name2) => !excludes.some((excluded) => overlayEntryNamed(excluded, name2))) : [];
@@ -125087,6 +125094,8 @@ async function enqueueTriggerFire(plan, options = {}) {
125087
125094
  const canary = options.packageCanary ?? null;
125088
125095
  const id7 = (canary ? manualCanaryJobId : manualTriggerJobId)({ triggerId: current.schedulerId, now: instant7 });
125089
125096
  redis = redisFn(url);
125097
+ redis.on?.("error", () => {
125098
+ });
125090
125099
  if (await redis.set(`trigger-fire:${current.queue}:${id7}`, "1", "EX", 120, "NX") !== "OK") return { refusal: "This cron trigger was already fired this minute. Nothing was queued.", deduplicated: true };
125091
125100
  queue = queueFn(parseConnectionFn(url, { failFast: true }), { name: current.queue });
125092
125101
  const schedule = canary ? { ...current.schedule, data: withJobDataVersion({ ...current.schedule.data, packageCanary: canary }) } : current.schedule;
@@ -127581,6 +127590,8 @@ async function checkDispatchRun({
127581
127590
  let redis;
127582
127591
  try {
127583
127592
  redis = redisFn(valkeyUrl);
127593
+ redis.on?.("error", () => {
127594
+ });
127584
127595
  const key2 = hourKey(now());
127585
127596
  const count7 = Number(consumeRate ? await redis.incr(key2) : await redis.get(key2) ?? 0);
127586
127597
  if (consumeRate && count7 === 1) await redis.expire(key2, DISPATCH_RUN_TTL_SECONDS);
@@ -128454,6 +128465,8 @@ async function readHosts({ url, redisFn = makeRedisClient, timeoutMs = 2500, now
128454
128465
  try {
128455
128466
  parseConnection(url, { failFast: true });
128456
128467
  redis = redisFn(url);
128468
+ redis.on?.("error", () => {
128469
+ });
128457
128470
  const res = await withTimeout2(readLiveHosts(redis, { now, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) }), timeoutMs, { unreachable: "timed out reaching the registry" });
128458
128471
  if (res.unreachable) return { unreachable: res.unreachable };
128459
128472
  return { hosts: res.hosts };
@@ -128477,6 +128490,8 @@ async function readHeldJobs({ url, limit: limit3 = 20, redisFn = makeRedisClient
128477
128490
  try {
128478
128491
  parseConnection(url, { failFast: true });
128479
128492
  redis = redisFn(url);
128493
+ redis.on?.("error", () => {
128494
+ });
128480
128495
  const settled = (async () => {
128481
128496
  const ids = await redis.smembers(HELD_SET);
128482
128497
  const hydrated = await Promise.all(
@@ -128545,6 +128560,8 @@ async function cancelHeldJob({ url, jobId, redisFn = makeRedisClient, queueFn =
128545
128560
  try {
128546
128561
  parseConnection(url, { failFast: true });
128547
128562
  redis = redisFn(url);
128563
+ redis.on?.("error", () => {
128564
+ });
128548
128565
  settled = (async () => {
128549
128566
  const { names: names2 } = await readFleetQueues({ url, redisFn, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) });
128550
128567
  let owner2 = null;
@@ -128677,6 +128694,8 @@ async function readBudget({ url, redisFn = makeRedisClient, timeoutMs = 2500, no
128677
128694
  try {
128678
128695
  parseConnection(url, { failFast: true });
128679
128696
  redis = redisFn(url);
128697
+ redis.on?.("error", () => {
128698
+ });
128680
128699
  const settled = Promise.all([redis.get(dayKey(now)), redis.get(weekKey(now)), redis.get(monthKey(now)), redis.get(tokenDayKey(now))]).then(
128681
128700
  ([day2, week, month, tokens]) => ({ day: Number(day2 ?? 0), week: Number(week ?? 0), month: Number(month ?? 0), tokensToday: Number(tokens ?? 0) }),
128682
128701
  (err) => ({ unreachable: err?.message ?? String(err) })
@@ -138104,7 +138123,10 @@ async function readConnections({ deploymentDir, env: env2 = {}, paths = {}, snap
138104
138123
  if (redis) return redis;
138105
138124
  if (owned) return owned;
138106
138125
  if (injectedRead && typeof paths.valkeyUrl !== "string") return null;
138107
- return owned = redisFn(paths.valkeyUrl);
138126
+ owned = redisFn(paths.valkeyUrl);
138127
+ owned.on?.("error", () => {
138128
+ });
138129
+ return owned;
138108
138130
  };
138109
138131
  const endpointFleetDeclared = endpointFleetDeclaration(file, environmentUnknown);
138110
138132
  let endpointStatus = { state: "unknown", hosts: [], more: 0, partial: true };
@@ -161477,362 +161499,6 @@ var init_models_view = __esm({
161477
161499
  }
161478
161500
  });
161479
161501
 
161480
- // src/console/panels/sandboxes.mjs
161481
- function state2(v) {
161482
- const s2 = report(v)?.request;
161483
- if (!s2) return null;
161484
- const success = s2.state === "ready";
161485
- return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
161486
- }
161487
- function sandboxFromRun(v) {
161488
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161489
- const all = rows(v);
161490
- const id7 = v.query?.id;
161491
- const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
161492
- const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
161493
- const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
161494
- if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
161495
- if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
161496
- const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
161497
- const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
161498
- const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
161499
- const what2 = facts([["Image", picked.image ?? "As recorded in its kept manifest"], ["/workspace", workspace], ["/job", "Read only, with the retained job inputs"], ["pi and what it loads", wordsHtml(loads)], ["Processes", "None until you start them"], ["Credentials", "None, on purpose"], ["Network", "The host's current job egress policy"]], { cols: 1 });
161500
- return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
161501
- }
161502
- function sandboxNew(v) {
161503
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161504
- const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
161505
- const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
161506
- const triggers2 = options;
161507
- const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
161508
- const fields3 = [
161509
- field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
161510
- field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
161511
- field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
161512
- field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
161513
- field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
161514
- ];
161515
- const plan = data5?.plan;
161516
- const source2 = data5?.fromRun ? html`<p class="sx-p">Preparing from <a class="link" href="${`/run?${new URLSearchParams({ id: data5.fromRun })}`}">${data5.fromRun}</a>. The ref starts at HEAD because the record holds no created pull request. ${data5.sourceRun?.historicalIndex !== null && data5.sourceRun?.historicalIndex !== void 0 ? `Its historical trigger row was ${data5.sourceRun.historicalIndex + 1}; that does not prove the current row's identity.` : "Its original trigger identity is unavailable."} Choose the current trigger explicitly.${String(data5.sourceRun?.backend).startsWith("openshell") ? " Its original OpenShell venue cannot prepare a sandbox; deliberately choose a compatible local or podman trigger." : ""}</p>` : null;
161517
- const form = html`${source2}<form method="get" action="/runs/sandboxes" data-live-hold>${data5?.fromRun ? html`<input type="hidden" name="fromRun" value="${data5.fromRun}">` : null}<div class="sx-form">${fields3}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
161518
- const fresh = plan ? html`${facts([[`On ${plan.host}`, wordsHtml(sandboxFreshLine({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }))]], { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
161519
- const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts5(plan)}${fresh}<p class="sx-p">${plan.pin ? `Pinned for ${plan.pinDays} days` : `Kept for ${plan.retentionHours} hours`}, on ${plan.host}. The worker clones on that host with a credential for this repository, then drops it. No container starts, no model runs, no dollars are spent and no run slot is taken.</p>${acts2(v) ? actForm({ action: "sandbox-prepare", hidden: [["repo", s2.repo], ["index", s2.index, "int"], ["ref", s2.ref], ["host", s2.host], ["pin", Boolean(s2.pin), "json"]], label: "Preview preparation", kind: ["primary"], icon: "plus" }) : html`<p class="card__sub">This console is read-only. Prepare from the host's terminal.</p>`}${cmd(["pi-dispatch", "sandbox", "--new", s2.repo, "--ref", s2.ref, "--as", String(s2.index), ...s2.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s2.index + 1} is index ${s2.index}.</p>` : html`<p class="sx-p" role="status">${data5?.refusal ?? "Choose a repository, its trigger, a ref and a host, then review their settings."}</p>`;
161520
- return card3("Without a run", html`${form}${preparation}<noscript><p class="card__sub">${SANDBOX_WORDS.noScript}</p></noscript>`, "a fresh clone with a trigger's image and a job's isolation");
161521
- }
161522
- function sandboxOpen(v) {
161523
- if (!allowed3(v)) return null;
161524
- const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
161525
- const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
161526
- const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
161527
- if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
161528
- return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
161529
- }
161530
- function sandboxKept(v) {
161531
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161532
- const data5 = report(v), all = rows(v);
161533
- return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
161534
- }
161535
- function sandboxBrowser(v) {
161536
- if (!allowed3(v) || v.query?.gatewaySelect) return null;
161537
- const gateway = v.data?.browserGateways;
161538
- const count7 = gateway?.enabledHosts?.length ?? 0;
161539
- return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
161540
- }
161541
- var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
161542
- var init_sandboxes = __esm({
161543
- "src/console/panels/sandboxes.mjs"() {
161544
- init_html();
161545
- init_kit();
161546
- init_forges();
161547
- init_sandbox_links();
161548
- init_common();
161549
- init_loaded_words();
161550
- init_loaded_parts();
161551
- SANDBOX_WORDS = Object.freeze({
161552
- unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
161553
- empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
161554
- noShells: "No live sandbox shell was registered. A shell whose terminal stopped answering disappears when its short lease expires. An expired registration is not proof the container stopped; inspect its host if the terminal ended unexpectedly.",
161555
- browser: "A separate gateway can attach to an already-open sandbox. It is off by default and enabled by hand per host. Open in Terminal and copy-command support remain available.",
161556
- noScript: "Preparing, pinning, ending a shell and saving terminal targets require JavaScript for Preview and Confirm. Commands remain selectable and the settings form works without JavaScript."
161557
- });
161558
- report = (v) => v.data?.sandboxes;
161559
- allowed3 = (v) => v.role === "operator";
161560
- acts2 = (v) => allowed3(v) && !v.readOnly;
161561
- rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
161562
- card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
161563
- stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
161564
- rowId2 = (r2) => r2.id ?? r2.jobId;
161565
- canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
161566
- selected3 = (v) => report(v)?.selected ?? {};
161567
- facts5 = (plan) => facts([
161568
- ["Image", plan.image ?? "not published"],
161569
- ["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
161570
- ["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
161571
- ["Credentials", "None in the shell"]
161572
- ]);
161573
- links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
161574
- retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
161575
- pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
161576
- SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
161577
- }
161578
- });
161579
-
161580
- // src/zero-touch-view.mjs
161581
- function breakdown(row2, repository = false) {
161582
- const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
161583
- if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
161584
- return out;
161585
- }
161586
- function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
161587
- const observed = raw?.coverage ?? raw?.window;
161588
- const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
161589
- if (!raw || typeof raw !== "object" || raw.state === "unavailable" || unobserved2) return { state: "unavailable", nowMs: nowMs2, windowDays, coverage: { partial: true, reasons: [] }, totals: {}, projects: [], causes: causes([]), trend: [], groups: [], fixes: [], fixesPartial: true, touchedRunIds: [], waitingRuns: [], ungroupedRunIds: [], grouping: { enabled: false, pending: 0, unassigned: 0, state: "disabled", sources: [] } };
161590
- const totals = raw.totals ?? raw.summary ?? {};
161591
- const coverage2 = raw.coverage ?? raw.window ?? {};
161592
- const out = {
161593
- state: "ready",
161594
- nowMs: number3(raw.nowMs) ?? nowMs2,
161595
- windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
161596
- coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
161597
- totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
161598
- projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
161599
- causes: causes(raw.causes),
161600
- trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
161601
- waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
161602
- touchedRunIds: runIds(raw.touchedRunIds),
161603
- ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
161604
- groups: [],
161605
- fixes: [],
161606
- fixesPartial: raw.fixesPartial === true,
161607
- grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
161608
- };
161609
- out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
161610
- if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
161611
- return out;
161612
- }
161613
- var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
161614
- var init_zero_touch_view = __esm({
161615
- "src/zero-touch-view.mjs"() {
161616
- CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
161617
- id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
161618
- number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
161619
- count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
161620
- text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
161621
- scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
161622
- causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
161623
- runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
161624
- }
161625
- });
161626
-
161627
- // src/console/panels/zero-touch.mjs
161628
- function delta(n3, previousRate = null) {
161629
- if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
161630
- return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
161631
- }
161632
- function coverage(m2) {
161633
- return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
161634
- }
161635
- function spark(m2) {
161636
- const rows3 = m2.trend;
161637
- if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
161638
- const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
161639
- const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
161640
- const y = (n3) => 56 - n3 / top * 56;
161641
- const segments = [];
161642
- let points = [];
161643
- for (const [i2, row2] of rows3.entries()) {
161644
- if (row2.rate === null || row2.partial) {
161645
- if (points.length) segments.push(points);
161646
- points = [];
161647
- } else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
161648
- }
161649
- if (points.length) segments.push(points);
161650
- const last = rows3[rows3.length - 1];
161651
- const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
161652
- return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
161653
- }
161654
- function longest(m2) {
161655
- return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
161656
- }
161657
- function waitingHistory(m2) {
161658
- return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
161659
- }
161660
- function compact(m2) {
161661
- const best = longest(m2);
161662
- return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
161663
- }
161664
- function zeroTouchRate(view) {
161665
- const m2 = model5(view);
161666
- return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
161667
- }
161668
- function zeroTouchStreak(view) {
161669
- const m2 = model5(view), best = longest(m2);
161670
- return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
161671
- }
161672
- function zeroTouchWait(view) {
161673
- const m2 = model5(view);
161674
- if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
161675
- return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
161676
- }
161677
- function zeroTouchHandled(view) {
161678
- const m2 = model5(view);
161679
- return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
161680
- }
161681
- function meter3(row2) {
161682
- if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
161683
- const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
161684
- const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
161685
- return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
161686
- }
161687
- function rowContent(row2, sub2 = false) {
161688
- const c = causesByKey.get(row2.topCause);
161689
- return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
161690
- }
161691
- function phoneProjects(m2, view) {
161692
- const item2 = (row2, repository = false) => {
161693
- const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
161694
- const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
161695
- const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
161696
- return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
161697
- };
161698
- return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
161699
- }
161700
- function zeroTouchProjects(view) {
161701
- const m2 = model5(view);
161702
- if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
161703
- const head = html`<div class="zt-row zt-row--head" aria-hidden="true"><span></span><span>Project</span><span>Touch rate, goal tick at 5%</span><span>Against the previous interval</span><span>Needed a person</span><span>Since the last touch</span><span>Most often</span></div>`;
161704
- const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
161705
- return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
161706
- }
161707
- function causeCounts(values) {
161708
- return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
161709
- }
161710
- function causeBar(values, max, name2) {
161711
- const rows3 = causeCounts(values);
161712
- let at2 = 0;
161713
- max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
161714
- const all = rows3.filter((r2) => r2.count > 0);
161715
- const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
161716
- return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
161717
- const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
161718
- at2 += r2.count;
161719
- return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
161720
- })}</svg>`;
161721
- }
161722
- function zeroTouchCauses(view) {
161723
- const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
161724
- if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
161725
- const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
161726
- const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
161727
- return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
161728
- }
161729
- function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
161730
- return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
161731
- }
161732
- function nameForm(g, index2) {
161733
- const key2 = `zt-name-${index2}`;
161734
- return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
161735
- }
161736
- function instructionForm(g, index2) {
161737
- if (!g.runIds[0] || g.triggerIndex === null) return html`<p class="zt-kps">Add to instructions is unavailable: a retained run and a current non-cron trigger are required.</p>`;
161738
- const key2 = `zt-sentence-${index2}`;
161739
- return html`<details class="zt-editor"><summary class="link">Add to instructions</summary>${guardedForm("zero-touch-instruction", { hidden: [["runId", g.runIds[0]], ["groupId", g.id], ["index", g.triggerIndex, "int"]], fields: field2({ label: "Sentence to add", id: key2, control: textarea({ id: key2, name: "sentence", required: true, rows: 3, maxlength: 2e3 }), hint: "Preview the exact diff, then Confirm. The trigger's existing instructions count toward the 2,000-character limit." }), label: `Preview trigger ${g.triggerIndex + 1}`, kind: ["sm", "primary"] })}</details>`;
161740
- }
161741
- function zeroTouchQuestions(view) {
161742
- if (view?.role !== "operator") return collapsed({ icon: "parked", title: "Questions it keeps asking", body: "are for operators. Rates, causes, streaks and fixes are shown here." });
161743
- const m2 = model5(view);
161744
- if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
161745
- const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
161746
- const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
161747
- const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
161748
- return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
161749
- }
161750
- function fixTone(f3) {
161751
- if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
161752
- return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
161753
- }
161754
- function fixObservation(f3) {
161755
- if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
161756
- if (f3.partial) return "Partial observation: effectiveness is not established.";
161757
- if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
161758
- return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
161759
- }
161760
- function beforeAfter(label3, value, max, tone = "attention") {
161761
- return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
161762
- }
161763
- function zeroTouchFixes(view) {
161764
- const m2 = model5(view);
161765
- const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
161766
- const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
161767
- return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
161768
- }
161769
- var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
161770
- var init_zero_touch = __esm({
161771
- "src/console/panels/zero-touch.mjs"() {
161772
- init_common();
161773
- init_html();
161774
- init_kit();
161775
- init_zero_touch_view();
161776
- ZERO_TOUCH_WORDS = Object.freeze({ unavailable: "Zero touch could not be read. Reload to try again; missing history is never counted as zero touches.", empty: "No workload runs ended in the covered interval.", drafts: "Sent by a person, edited or as drafted. Each run still counts as a touch.", draftsPartial: "Partial history; this counts retained runs only.", viewer: "Questions it keeps asking are for operators. Rates, causes, streaks and fixes are shown here.", noGroups: "No question groups yet. Group retained questions with a capped decision job; name each group yourself.", noFixes: "No cause-linked fixes recorded yet. Start a change from a refusal or a question group to measure it here." });
161777
- ZERO_TOUCH_CAUSES = Object.freeze([
161778
- { cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
161779
- { cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
161780
- { cause: "rerun", label: "Failed or refused, then re-run", short: "Re-run", icon: "failed", tone: "failed", detail: "A person started a failed or refused run again." },
161781
- { cause: "config-fix", label: "Refused until config was fixed", short: "Config fixed", icon: "refused", tone: "refused", detail: "A confirmed configuration change explicitly targeted the refusal." },
161782
- { cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
161783
- { cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
161784
- ]);
161785
- ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
161786
- unassigned: "Not grouped yet.",
161787
- pending: "A grouping job is pending.",
161788
- assigned: "Assigned to a question group.",
161789
- "source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
161790
- "source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
161791
- "source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
161792
- "candidates-expired": "A group's representative context expired. Review the groups before trying again.",
161793
- "group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
161794
- stale: "The group revision changed during classification. Preview a fresh grouping job.",
161795
- refused: "The grouping job was refused. Open its run to see the policy reason.",
161796
- failed: "The grouping job failed. Open its run and review the failure before trying again."
161797
- });
161798
- causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
161799
- model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
161800
- acts3 = (v) => v?.role === "operator" && !v?.readOnly;
161801
- pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
161802
- amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
161803
- date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
161804
- span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
161805
- link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
161806
- fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
161807
- ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
161808
- ZERO_TOUCH_PANELS = Object.freeze([["p-zt-rate", zeroTouchRate], ["p-zt-streak", zeroTouchStreak], ["p-zt-wait", zeroTouchWait], ["p-zt-handled", zeroTouchHandled], ["p-zt-projects", zeroTouchProjects], ["p-zt-causes", zeroTouchCauses], ["p-zt-questions", zeroTouchQuestions], ["p-zt-fixes", zeroTouchFixes]]);
161809
- }
161810
- });
161811
-
161812
- // src/trigger-policy.mjs
161813
- function triggerWhoMayFire(trigger2) {
161814
- const on = trigger2.on ?? trigger2;
161815
- const forge = trigger2.run?.kind ?? trigger2.forge;
161816
- const kind = on.type;
161817
- if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
161818
- if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
161819
- if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
161820
- if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
161821
- if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
161822
- if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
161823
- const actions = Array.isArray(on.action) ? on.action : [];
161824
- const gates = [];
161825
- if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
161826
- if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
161827
- if (actions.includes("review_submitted")) gates.push("The reviewer's association must be OWNER, MEMBER or COLLABORATOR, regardless of the pull request author's association");
161828
- if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
161829
- return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
161830
- }
161831
- var init_trigger_policy = __esm({
161832
- "src/trigger-policy.mjs"() {
161833
- }
161834
- });
161835
-
161836
161502
  // src/trigger-gets-view.mjs
161837
161503
  function cannotSay(row2, trigger2, fileVersion) {
161838
161504
  if (fileVersion > 1) {
@@ -161875,6 +161541,7 @@ function flowAnswer(row2, trigger2) {
161875
161541
  const found = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow);
161876
161542
  if (!found) return null;
161877
161543
  if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
161544
+ if (found.tier === "package" && trigger2.packages === false) return null;
161878
161545
  return found.tier === "package" ? `from ${found.package ?? "a staged package"}` : FLOW_TIER_WORDS[found.tier] ?? null;
161879
161546
  }
161880
161547
  function lacksFlow(row2, trigger2) {
@@ -162014,7 +161681,7 @@ function overlayStem(segment) {
162014
161681
  for (let end = 1; end < segment.length; end += 1) if (overlayEntryNamed(segment.slice(0, end), segment)) return segment.slice(0, end);
162015
161682
  return segment;
162016
161683
  }
162017
- function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
161684
+ function extensionsRow2({ trigger: trigger2, rows: rows3, last, records = [], historyUnread = false, hostLoads = null }) {
162018
161685
  const known2 = rows3.filter((row2) => row2.state === "current");
162019
161686
  const inFile = trigger2.excludeExtensions ?? [];
162020
161687
  const withheld = trigger2.packages === false;
@@ -162030,10 +161697,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162030
161697
  for (const value of naming.length > 0 ? naming : [overlayStem(segment)]) groupOf(value).segments.add(segment);
162031
161698
  }
162032
161699
  for (const name2 of inFile) groupOf(name2);
162033
- const brought = (name2, version = null) => {
162034
- const row2 = (last?.sources ?? []).find((source2) => source2.name === name2 && (version === null || source2.version === version));
162035
- const words = broughtWords(row2);
162036
- return words === null ? null : `${words}, as ${last.id} loaded it`;
161700
+ const said = (Array.isArray(records) ? records : []).map((record3) => triggerLastRun([record3], hostLoads).quoted).filter(Boolean);
161701
+ const brought = (name2, { kind, version = null }) => {
161702
+ for (const run3 of [last, ...said].filter(Boolean)) {
161703
+ const row2 = (run3.sources ?? []).find((source2) => source2.kind === kind && source2.name === name2 && (version === null || source2.version === version));
161704
+ if (row2) return `${broughtWords(row2) ?? "no tool, skill or prompt template"}, as ${run3.id} loaded it`;
161705
+ }
161706
+ return null;
162037
161707
  };
162038
161708
  const items = [...groups.values()].map(({ value, versions: versions2, segments }) => {
162039
161709
  const on = !inFile.includes(value);
@@ -162045,12 +161715,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162045
161715
  const off = known2.filter((row2) => holdsEntry(row2) && row2.overlayExtensionsAllowed !== true).map((row2) => row2.name);
162046
161716
  const hostsOn = (version) => known2.filter((row2) => (row2.packages ?? []).some((entry3) => entry3.name === value && entry3.version === version)).map((row2) => row2.name);
162047
161717
  const stagedWhere = staged.length === 0 ? null : whereWords(rows3, "packages", holdsPackage, "staged", staged.length === 1 ? null : staged.map((version) => `${version} on ${listed(hostsOn(version))}`).join(", "));
161718
+ const fromEntries = () => entries.map((segment) => brought(segment, { kind: "overlay" })).find((words) => words !== null) ?? null;
162048
161719
  const quoted = (() => {
162049
- if (!on || withheld && entries.length === 0) return null;
162050
- if (staged.length === 0) return brought(entries[0]);
162051
- const running = staged.find((version) => brought(value, version) !== null);
162052
- if (running === void 0) return entries.length > 0 ? brought(entries[0]) : null;
162053
- return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, running)}`;
161720
+ if (withheld && entries.length === 0) return null;
161721
+ if (staged.length === 0 || withheld) return fromEntries();
161722
+ const running = staged.find((version) => brought(value, { kind: "package", version }) !== null);
161723
+ if (running === void 0) return fromEntries();
161724
+ return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, { kind: "package", version: running })}`;
162054
161725
  })();
162055
161726
  const also = inFile.includes(value) ? alsoNaming(inFile, value) : [];
162056
161727
  const named2 = staged.length > 0 ? `${value} ${listed(staged)}` : entries.length > 0 && also.length === 0 ? entries.join(", ") : value;
@@ -162060,7 +161731,9 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
162060
161731
  entries.length > 0 ? whereWords(rows3, "extensions", holdsEntry, "in the overlay") : null,
162061
161732
  off.length > 0 ? `its overlay extensions off on ${listed(off)}` : null,
162062
161733
  also.length > 0 ? `the file also names ${listed(also)}` : null,
162063
- quoted
161734
+ quoted,
161735
+ // Not said where the history could not be read: the card says so once, and a failed read is not "no run".
161736
+ quoted === null && !historyUnread && !(withheld && entries.length === 0) && (staged.length > 0 || entries.length > 0) ? staged.length > 0 && !withheld ? TRIGGER_GETS_WORDS.bringsUnknown : TRIGGER_GETS_WORDS.overlayBringsUnknown : null
162064
161737
  ].filter(Boolean).join(" \xB7 ");
162065
161738
  return { value, name: named2, on, moot, sub: sub2, backOn: also.length > 0 ? `${value} off the list` : `${value} back on` };
162066
161739
  });
@@ -162163,14 +161836,67 @@ function triggerGetsOf({ trigger: trigger2, records = [], historyUnread = false,
162163
161836
  last: { Model: under("Model"), Skills: under("Skills"), Extensions: under("Extensions"), MCP: under("MCP"), Tools: under("Tools"), pi: under("pi") },
162164
161837
  model: modelRow2({ trigger: trigger2, defaults: defaults4, modelChoices }),
162165
161838
  flow: { ...flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }), ...watch ? { doctor: null, lacking: null } : {} },
162166
- extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted }),
161839
+ extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted, records, historyUnread, hostLoads }),
162167
161840
  mcp: mcpRow2({ trigger: trigger2, rows: rows3, overlayServers }),
162168
161841
  tools: toolsRow2({ trigger: trigger2, last: quoted }),
162169
161842
  image: imageRow({ trigger: trigger2, rows: rows3 }),
162170
161843
  secrets: { profile: trigger2.secretsProfile ?? null, count: trigger2.secrets ?? 0, optedOut: trigger2.secretBundle === false }
162171
161844
  };
162172
161845
  }
162173
- var TRIGGER_GETS_WORDS, code4, run2, time2, unique, overlayServersOf, packagesLoaded, overlayLoaded, serverDefinition, FLOW_TIER_WORDS, MODEL_CHOICES_MAX, PROVIDER_CHOICES_MAX, FLOW_CHOICES_MAX, listsWhole, plural7, alsoNaming;
161846
+ function freshFlowWords(row2, trigger2) {
161847
+ if (trigger2.flow === null) return SANDBOX_FRESH_WORDS.noFlow;
161848
+ const who = `a job of trigger ${trigger2.index + 1} on this host`;
161849
+ const word3 = flowAnswer(row2, trigger2);
161850
+ if (word3 === null) return `Whether ${who} would find ${trigger2.flow} is unknown: its last doctor run has no answer for it.`;
161851
+ const tier = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow)?.tier;
161852
+ if (tier === "none" || tier === "unknown") return `Whether ${who} would find ${trigger2.flow} is unknown: doctor found it ${word3}, and the repository, which is on no host, may hold it.`;
161853
+ if (tier === "repository") return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}.`;
161854
+ return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}, unless the repository holds it at its base commit, which comes first.`;
161855
+ }
161856
+ function freshMcpWords(row2, named2) {
161857
+ if (!Array.isArray(named2) || named2.length === 0) return null;
161858
+ const on = [];
161859
+ const off = [];
161860
+ const lacking = [];
161861
+ const unknown3 = [];
161862
+ let forge = false;
161863
+ for (const name2 of named2) {
161864
+ if (name2 === FORGE_MCP_SERVER_NAME) {
161865
+ forge = true;
161866
+ continue;
161867
+ }
161868
+ const server = (row2.mcp ?? []).find((entry3) => entry3.name === name2);
161869
+ if (server) (server.enabled === true ? on : off).push(name2);
161870
+ else (freshListsWhole(row2, "mcp") ? lacking : unknown3).push(name2);
161871
+ }
161872
+ const refused3 = [off.length > 0 ? `${listed(off)} switched off here` : null, lacking.length > 0 ? `${listed(lacking)} not on this host` : null].filter(Boolean);
161873
+ const said = [
161874
+ on.length > 0 ? `${listed(on)} on this host` : null,
161875
+ // The reason is not named: a host with no mcp.json publishes the same empty list, and its jobs are refused as
161876
+ // mcp-config-invalid rather than mcp-server-unknown (`serverRefused`). Either way, before it spends.
161877
+ refused3.length > 0 ? `${refused3.join(" and ")}, so a job there is refused before it spends` : null,
161878
+ unknown3.length > 0 ? `${listed(unknown3)} unknown here` : null,
161879
+ forge ? `${FORGE_MCP_SERVER_NAME}: ${TRIGGER_GETS_WORDS.forgeServer}` : null
161880
+ ].filter(Boolean);
161881
+ return `${said.join("; ")}.`;
161882
+ }
161883
+ function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
161884
+ const rows3 = [[`On ${host}`, sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages, excludes, image, mcp })]];
161885
+ const row2 = hostLoadsRow(hostLoads, host);
161886
+ if (row2.state === "unknown") return rows3;
161887
+ const named2 = typeof flow === "string" && flow !== "" ? flow : null;
161888
+ if (row2.state !== "current") {
161889
+ rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [named2 === null ? SANDBOX_FRESH_WORDS.noFlow : `Whether a job of trigger ${triggerIndex + 1} on this host would find ${named2} is unknown: its last doctor run is old.`]]);
161890
+ const listedNames = (mcp ?? []).filter((name2) => typeof name2 === "string");
161891
+ if (listedNames.length > 0) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [`${listed(listedNames)} unknown here: its last doctor run is old.`]]);
161892
+ return rows3;
161893
+ }
161894
+ rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0 })]]);
161895
+ const mcpWords = freshMcpWords(row2, mcp);
161896
+ if (mcpWords !== null) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [mcpWords]]);
161897
+ return rows3;
161898
+ }
161899
+ var TRIGGER_GETS_WORDS, code4, run2, time2, unique, overlayServersOf, packagesLoaded, overlayLoaded, serverDefinition, FLOW_TIER_WORDS, MODEL_CHOICES_MAX, PROVIDER_CHOICES_MAX, FLOW_CHOICES_MAX, listsWhole, plural7, alsoNaming, SANDBOX_FRESH_WORDS, freshListsWhole;
162174
161900
  var init_trigger_gets_view = __esm({
162175
161901
  "src/trigger-gets-view.mjs"() {
162176
161902
  init_mcp_config();
@@ -162206,6 +161932,8 @@ var init_trigger_gets_view = __esm({
162206
161932
  noExtensions: "The hosts that can say name no staged package or overlay extension.",
162207
161933
  noExtensionsKnown: "No host\u2019s doctor run can say which extensions it has.",
162208
161934
  forgeServer: "the job\u2019s own forge, with its own token; in no overlay",
161935
+ bringsUnknown: "what it brings is known once a run has loaded it",
161936
+ overlayBringsUnknown: "an overlay entry has no manifest, so what it brings is known once a run has loaded it",
162209
161937
  noServers: "The hosts that can say name no MCP server, and this trigger names none.",
162210
161938
  noServersKnown: "No host\u2019s doctor run can say which MCP servers it has, and this trigger names none.",
162211
161939
  serverRefused: "A job on a host without a server it names, or with it off, is refused as mcp-server-unknown, before it spends; a host with no overlay or no mcp.json refuses it as mcp-config-invalid. A key a server uses must reach the job, through run.secrets in the file or a host\u2019s PI_FORWARD_ENV, or the job is refused as mcp-secret-unmapped.",
@@ -162251,6 +161979,369 @@ var init_trigger_gets_view = __esm({
162251
161979
  listsWhole = (row2, part) => row2.state === "current" && !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
162252
161980
  plural7 = (n3, one, many = `${one}s`) => `${n3} ${n3 === 1 ? one : many}`;
162253
161981
  alsoNaming = (list5, name2) => list5.filter((other) => other !== name2 && (overlayEntryNamed(other, name2) || overlayEntryNamed(name2, other)));
161982
+ SANDBOX_FRESH_WORDS = Object.freeze({
161983
+ noFlow: "This trigger names no flow.",
161984
+ flowLabel: "Flow",
161985
+ mcpLabel: "MCP servers"
161986
+ });
161987
+ freshListsWhole = (row2, part) => !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
161988
+ }
161989
+ });
161990
+
161991
+ // src/console/panels/sandboxes.mjs
161992
+ function state2(v) {
161993
+ const s2 = report(v)?.request;
161994
+ if (!s2) return null;
161995
+ const success = s2.state === "ready";
161996
+ return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
161997
+ }
161998
+ function sandboxFromRun(v) {
161999
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162000
+ const all = rows(v);
162001
+ const id7 = v.query?.id;
162002
+ const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
162003
+ const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
162004
+ const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
162005
+ if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
162006
+ if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
162007
+ const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
162008
+ const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
162009
+ const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
162010
+ const what2 = facts([["Image", picked.image ?? "As recorded in its kept manifest"], ["/workspace", workspace], ["/job", "Read only, with the retained job inputs"], ["pi and what it loads", wordsHtml(loads)], ["Processes", "None until you start them"], ["Credentials", "None, on purpose"], ["Network", "The host's current job egress policy"]], { cols: 1 });
162011
+ return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
162012
+ }
162013
+ function sandboxNew(v) {
162014
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162015
+ const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
162016
+ const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
162017
+ const triggers2 = options;
162018
+ const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
162019
+ const fields3 = [
162020
+ field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
162021
+ field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
162022
+ field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
162023
+ field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
162024
+ field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
162025
+ ];
162026
+ const plan = data5?.plan;
162027
+ const source2 = data5?.fromRun ? html`<p class="sx-p">Preparing from <a class="link" href="${`/run?${new URLSearchParams({ id: data5.fromRun })}`}">${data5.fromRun}</a>. The ref starts at HEAD because the record holds no created pull request. ${data5.sourceRun?.historicalIndex !== null && data5.sourceRun?.historicalIndex !== void 0 ? `Its historical trigger row was ${data5.sourceRun.historicalIndex + 1}; that does not prove the current row's identity.` : "Its original trigger identity is unavailable."} Choose the current trigger explicitly.${String(data5.sourceRun?.backend).startsWith("openshell") ? " Its original OpenShell venue cannot prepare a sandbox; deliberately choose a compatible local or podman trigger." : ""}</p>` : null;
162028
+ const form = html`${source2}<form method="get" action="/runs/sandboxes" data-live-hold>${data5?.fromRun ? html`<input type="hidden" name="fromRun" value="${data5.fromRun}">` : null}<div class="sx-form">${fields3}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
162029
+ const fresh = plan ? html`${facts(sandboxFreshRows({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, flow: data5?.flow ?? null, mcp: data5?.mcp ?? [], withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }).map(([label3, parts2]) => [label3, wordsHtml(parts2)]), { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
162030
+ const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts5(plan)}${fresh}<p class="sx-p">${plan.pin ? `Pinned for ${plan.pinDays} days` : `Kept for ${plan.retentionHours} hours`}, on ${plan.host}. The worker clones on that host with a credential for this repository, then drops it. No container starts, no model runs, no dollars are spent and no run slot is taken.</p>${acts2(v) ? actForm({ action: "sandbox-prepare", hidden: [["repo", s2.repo], ["index", s2.index, "int"], ["ref", s2.ref], ["host", s2.host], ["pin", Boolean(s2.pin), "json"]], label: "Preview preparation", kind: ["primary"], icon: "plus" }) : html`<p class="card__sub">This console is read-only. Prepare from the host's terminal.</p>`}${cmd(["pi-dispatch", "sandbox", "--new", s2.repo, "--ref", s2.ref, "--as", String(s2.index), ...s2.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s2.index + 1} is index ${s2.index}.</p>` : html`<p class="sx-p" role="status">${data5?.refusal ?? "Choose a repository, its trigger, a ref and a host, then review their settings."}</p>`;
162031
+ return card3("Without a run", html`${form}${preparation}<noscript><p class="card__sub">${SANDBOX_WORDS.noScript}</p></noscript>`, "a fresh clone with a trigger's image and a job's isolation");
162032
+ }
162033
+ function sandboxOpen(v) {
162034
+ if (!allowed3(v)) return null;
162035
+ const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
162036
+ const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
162037
+ const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
162038
+ if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
162039
+ return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
162040
+ }
162041
+ function sandboxKept(v) {
162042
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162043
+ const data5 = report(v), all = rows(v);
162044
+ return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
162045
+ }
162046
+ function sandboxBrowser(v) {
162047
+ if (!allowed3(v) || v.query?.gatewaySelect) return null;
162048
+ const gateway = v.data?.browserGateways;
162049
+ const count7 = gateway?.enabledHosts?.length ?? 0;
162050
+ return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
162051
+ }
162052
+ var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
162053
+ var init_sandboxes = __esm({
162054
+ "src/console/panels/sandboxes.mjs"() {
162055
+ init_html();
162056
+ init_kit();
162057
+ init_forges();
162058
+ init_sandbox_links();
162059
+ init_common();
162060
+ init_loaded_words();
162061
+ init_loaded_parts();
162062
+ init_trigger_gets_view();
162063
+ SANDBOX_WORDS = Object.freeze({
162064
+ unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
162065
+ empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
162066
+ noShells: "No live sandbox shell was registered. A shell whose terminal stopped answering disappears when its short lease expires. An expired registration is not proof the container stopped; inspect its host if the terminal ended unexpectedly.",
162067
+ browser: "A separate gateway can attach to an already-open sandbox. It is off by default and enabled by hand per host. Open in Terminal and copy-command support remain available.",
162068
+ noScript: "Preparing, pinning, ending a shell and saving terminal targets require JavaScript for Preview and Confirm. Commands remain selectable and the settings form works without JavaScript."
162069
+ });
162070
+ report = (v) => v.data?.sandboxes;
162071
+ allowed3 = (v) => v.role === "operator";
162072
+ acts2 = (v) => allowed3(v) && !v.readOnly;
162073
+ rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
162074
+ card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
162075
+ stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
162076
+ rowId2 = (r2) => r2.id ?? r2.jobId;
162077
+ canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
162078
+ selected3 = (v) => report(v)?.selected ?? {};
162079
+ facts5 = (plan) => facts([
162080
+ ["Image", plan.image ?? "not published"],
162081
+ ["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
162082
+ ["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
162083
+ ["Credentials", "None in the shell"]
162084
+ ]);
162085
+ links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
162086
+ retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
162087
+ pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
162088
+ SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
162089
+ }
162090
+ });
162091
+
162092
+ // src/zero-touch-view.mjs
162093
+ function breakdown(row2, repository = false) {
162094
+ const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
162095
+ if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
162096
+ return out;
162097
+ }
162098
+ function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
162099
+ const observed = raw?.coverage ?? raw?.window;
162100
+ const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
162101
+ if (!raw || typeof raw !== "object" || raw.state === "unavailable" || unobserved2) return { state: "unavailable", nowMs: nowMs2, windowDays, coverage: { partial: true, reasons: [] }, totals: {}, projects: [], causes: causes([]), trend: [], groups: [], fixes: [], fixesPartial: true, touchedRunIds: [], waitingRuns: [], ungroupedRunIds: [], grouping: { enabled: false, pending: 0, unassigned: 0, state: "disabled", sources: [] } };
162102
+ const totals = raw.totals ?? raw.summary ?? {};
162103
+ const coverage2 = raw.coverage ?? raw.window ?? {};
162104
+ const out = {
162105
+ state: "ready",
162106
+ nowMs: number3(raw.nowMs) ?? nowMs2,
162107
+ windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
162108
+ coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
162109
+ totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
162110
+ projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
162111
+ causes: causes(raw.causes),
162112
+ trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
162113
+ waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
162114
+ touchedRunIds: runIds(raw.touchedRunIds),
162115
+ ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
162116
+ groups: [],
162117
+ fixes: [],
162118
+ fixesPartial: raw.fixesPartial === true,
162119
+ grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
162120
+ };
162121
+ out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
162122
+ if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
162123
+ return out;
162124
+ }
162125
+ var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
162126
+ var init_zero_touch_view = __esm({
162127
+ "src/zero-touch-view.mjs"() {
162128
+ CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
162129
+ id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
162130
+ number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
162131
+ count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
162132
+ text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
162133
+ scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
162134
+ causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
162135
+ runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
162136
+ }
162137
+ });
162138
+
162139
+ // src/console/panels/zero-touch.mjs
162140
+ function delta(n3, previousRate = null) {
162141
+ if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
162142
+ return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
162143
+ }
162144
+ function coverage(m2) {
162145
+ return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
162146
+ }
162147
+ function spark(m2) {
162148
+ const rows3 = m2.trend;
162149
+ if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
162150
+ const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
162151
+ const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
162152
+ const y = (n3) => 56 - n3 / top * 56;
162153
+ const segments = [];
162154
+ let points = [];
162155
+ for (const [i2, row2] of rows3.entries()) {
162156
+ if (row2.rate === null || row2.partial) {
162157
+ if (points.length) segments.push(points);
162158
+ points = [];
162159
+ } else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
162160
+ }
162161
+ if (points.length) segments.push(points);
162162
+ const last = rows3[rows3.length - 1];
162163
+ const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
162164
+ return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
162165
+ }
162166
+ function longest(m2) {
162167
+ return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
162168
+ }
162169
+ function waitingHistory(m2) {
162170
+ return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
162171
+ }
162172
+ function compact(m2) {
162173
+ const best = longest(m2);
162174
+ return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
162175
+ }
162176
+ function zeroTouchRate(view) {
162177
+ const m2 = model5(view);
162178
+ return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
162179
+ }
162180
+ function zeroTouchStreak(view) {
162181
+ const m2 = model5(view), best = longest(m2);
162182
+ return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
162183
+ }
162184
+ function zeroTouchWait(view) {
162185
+ const m2 = model5(view);
162186
+ if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
162187
+ return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
162188
+ }
162189
+ function zeroTouchHandled(view) {
162190
+ const m2 = model5(view);
162191
+ return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
162192
+ }
162193
+ function meter3(row2) {
162194
+ if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
162195
+ const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
162196
+ const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
162197
+ return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
162198
+ }
162199
+ function rowContent(row2, sub2 = false) {
162200
+ const c = causesByKey.get(row2.topCause);
162201
+ return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
162202
+ }
162203
+ function phoneProjects(m2, view) {
162204
+ const item2 = (row2, repository = false) => {
162205
+ const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
162206
+ const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
162207
+ const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
162208
+ return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
162209
+ };
162210
+ return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
162211
+ }
162212
+ function zeroTouchProjects(view) {
162213
+ const m2 = model5(view);
162214
+ if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
162215
+ const head = html`<div class="zt-row zt-row--head" aria-hidden="true"><span></span><span>Project</span><span>Touch rate, goal tick at 5%</span><span>Against the previous interval</span><span>Needed a person</span><span>Since the last touch</span><span>Most often</span></div>`;
162216
+ const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
162217
+ return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
162218
+ }
162219
+ function causeCounts(values) {
162220
+ return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
162221
+ }
162222
+ function causeBar(values, max, name2) {
162223
+ const rows3 = causeCounts(values);
162224
+ let at2 = 0;
162225
+ max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
162226
+ const all = rows3.filter((r2) => r2.count > 0);
162227
+ const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
162228
+ return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
162229
+ const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
162230
+ at2 += r2.count;
162231
+ return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
162232
+ })}</svg>`;
162233
+ }
162234
+ function zeroTouchCauses(view) {
162235
+ const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
162236
+ if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
162237
+ const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
162238
+ const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
162239
+ return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
162240
+ }
162241
+ function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
162242
+ return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
162243
+ }
162244
+ function nameForm(g, index2) {
162245
+ const key2 = `zt-name-${index2}`;
162246
+ return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
162247
+ }
162248
+ function instructionForm(g, index2) {
162249
+ if (!g.runIds[0] || g.triggerIndex === null) return html`<p class="zt-kps">Add to instructions is unavailable: a retained run and a current non-cron trigger are required.</p>`;
162250
+ const key2 = `zt-sentence-${index2}`;
162251
+ return html`<details class="zt-editor"><summary class="link">Add to instructions</summary>${guardedForm("zero-touch-instruction", { hidden: [["runId", g.runIds[0]], ["groupId", g.id], ["index", g.triggerIndex, "int"]], fields: field2({ label: "Sentence to add", id: key2, control: textarea({ id: key2, name: "sentence", required: true, rows: 3, maxlength: 2e3 }), hint: "Preview the exact diff, then Confirm. The trigger's existing instructions count toward the 2,000-character limit." }), label: `Preview trigger ${g.triggerIndex + 1}`, kind: ["sm", "primary"] })}</details>`;
162252
+ }
162253
+ function zeroTouchQuestions(view) {
162254
+ if (view?.role !== "operator") return collapsed({ icon: "parked", title: "Questions it keeps asking", body: "are for operators. Rates, causes, streaks and fixes are shown here." });
162255
+ const m2 = model5(view);
162256
+ if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
162257
+ const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
162258
+ const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
162259
+ const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
162260
+ return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
162261
+ }
162262
+ function fixTone(f3) {
162263
+ if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
162264
+ return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
162265
+ }
162266
+ function fixObservation(f3) {
162267
+ if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
162268
+ if (f3.partial) return "Partial observation: effectiveness is not established.";
162269
+ if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
162270
+ return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
162271
+ }
162272
+ function beforeAfter(label3, value, max, tone = "attention") {
162273
+ return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
162274
+ }
162275
+ function zeroTouchFixes(view) {
162276
+ const m2 = model5(view);
162277
+ const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
162278
+ const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
162279
+ return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
162280
+ }
162281
+ var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
162282
+ var init_zero_touch = __esm({
162283
+ "src/console/panels/zero-touch.mjs"() {
162284
+ init_common();
162285
+ init_html();
162286
+ init_kit();
162287
+ init_zero_touch_view();
162288
+ ZERO_TOUCH_WORDS = Object.freeze({ unavailable: "Zero touch could not be read. Reload to try again; missing history is never counted as zero touches.", empty: "No workload runs ended in the covered interval.", drafts: "Sent by a person, edited or as drafted. Each run still counts as a touch.", draftsPartial: "Partial history; this counts retained runs only.", viewer: "Questions it keeps asking are for operators. Rates, causes, streaks and fixes are shown here.", noGroups: "No question groups yet. Group retained questions with a capped decision job; name each group yourself.", noFixes: "No cause-linked fixes recorded yet. Start a change from a refusal or a question group to measure it here." });
162289
+ ZERO_TOUCH_CAUSES = Object.freeze([
162290
+ { cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
162291
+ { cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
162292
+ { cause: "rerun", label: "Failed or refused, then re-run", short: "Re-run", icon: "failed", tone: "failed", detail: "A person started a failed or refused run again." },
162293
+ { cause: "config-fix", label: "Refused until config was fixed", short: "Config fixed", icon: "refused", tone: "refused", detail: "A confirmed configuration change explicitly targeted the refusal." },
162294
+ { cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
162295
+ { cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
162296
+ ]);
162297
+ ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
162298
+ unassigned: "Not grouped yet.",
162299
+ pending: "A grouping job is pending.",
162300
+ assigned: "Assigned to a question group.",
162301
+ "source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
162302
+ "source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
162303
+ "source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
162304
+ "candidates-expired": "A group's representative context expired. Review the groups before trying again.",
162305
+ "group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
162306
+ stale: "The group revision changed during classification. Preview a fresh grouping job.",
162307
+ refused: "The grouping job was refused. Open its run to see the policy reason.",
162308
+ failed: "The grouping job failed. Open its run and review the failure before trying again."
162309
+ });
162310
+ causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
162311
+ model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
162312
+ acts3 = (v) => v?.role === "operator" && !v?.readOnly;
162313
+ pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
162314
+ amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
162315
+ date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
162316
+ span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
162317
+ link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
162318
+ fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
162319
+ ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
162320
+ ZERO_TOUCH_PANELS = Object.freeze([["p-zt-rate", zeroTouchRate], ["p-zt-streak", zeroTouchStreak], ["p-zt-wait", zeroTouchWait], ["p-zt-handled", zeroTouchHandled], ["p-zt-projects", zeroTouchProjects], ["p-zt-causes", zeroTouchCauses], ["p-zt-questions", zeroTouchQuestions], ["p-zt-fixes", zeroTouchFixes]]);
162321
+ }
162322
+ });
162323
+
162324
+ // src/trigger-policy.mjs
162325
+ function triggerWhoMayFire(trigger2) {
162326
+ const on = trigger2.on ?? trigger2;
162327
+ const forge = trigger2.run?.kind ?? trigger2.forge;
162328
+ const kind = on.type;
162329
+ if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
162330
+ if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
162331
+ if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
162332
+ if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
162333
+ if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
162334
+ if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
162335
+ const actions = Array.isArray(on.action) ? on.action : [];
162336
+ const gates = [];
162337
+ if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
162338
+ if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
162339
+ if (actions.includes("review_submitted")) gates.push("The reviewer's association must be OWNER, MEMBER or COLLABORATOR, regardless of the pull request author's association");
162340
+ if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
162341
+ return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
162342
+ }
162343
+ var init_trigger_policy = __esm({
162344
+ "src/trigger-policy.mjs"() {
162254
162345
  }
162255
162346
  });
162256
162347
 
@@ -174616,6 +174707,15 @@ import { createHash as createHash28 } from "node:crypto";
174616
174707
  function changesHtml(changes) {
174617
174708
  return (Array.isArray(changes) ? changes : []).map(([key2, before, after], i2) => html`${i2 > 0 ? ", " : null}${key2}: <b>${before}</b> to <b>${after}</b>`);
174618
174709
  }
174710
+ function plainBody(value, depth = 0) {
174711
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
174712
+ if (typeof value === "number") return Number.isFinite(value);
174713
+ if (depth >= BODY_DEPTH_MAX || typeof value !== "object") return false;
174714
+ if (Array.isArray(value)) return value.length <= 256 && value.every((v) => plainBody(v, depth + 1));
174715
+ const keys3 = Object.keys(value);
174716
+ if (keys3.length > BODY_KEYS_MAX) return false;
174717
+ return keys3.every((k) => plainBody(value[k], depth + 1));
174718
+ }
174619
174719
  function str4(body, key2, { required = false, max = 512 } = {}) {
174620
174720
  const v = body?.[key2];
174621
174721
  if (v === void 0 || v === null || v === "") {
@@ -175245,11 +175345,37 @@ function settingDone(out) {
175245
175345
  const sayable = typeof old === "string" && old.trim() !== "" || typeof old === "number" && Number.isFinite(old);
175246
175346
  return sayable ? `Changed ${key2} from ${String(old)} to ${value}` : `Changed ${key2} to ${value}`;
175247
175347
  }
175348
+ function pauseListShapeRefusal(windows) {
175349
+ for (const [i2, w] of windows.entries()) {
175350
+ const at2 = atIndex("pause window", i2);
175351
+ if (w === null || typeof w !== "object" || Array.isArray(w)) return `${at2} is not an object`;
175352
+ for (const [k, value] of Object.entries(w)) {
175353
+ if (!PAUSE_WINDOW_FIELDS.includes(k)) return `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only)`;
175354
+ const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
175355
+ if (!plain) return `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}`;
175356
+ }
175357
+ }
175358
+ return null;
175359
+ }
175248
175360
  function pauseListBack(out) {
175249
175361
  const unread2 = typeof out.beforeText === "string" && !Array.isArray(jsonOf(out.beforeText)?.windows);
175250
175362
  if (unread2) return { undo: twice(UNDO_SENTENCES.tooBig, UNDO_SENTENCES.tooBigAfter), inverse: null };
175363
+ if (!pauseListGoesBack(out)) return { undo: twice(UNDO_SENTENCES.refusedList, UNDO_SENTENCES.refusedListAfter), inverse: null };
175251
175364
  return { undo: "Put the previous pause windows back", inverse: { action: "pause-windows", body: { windows: out.extra.before } } };
175252
175365
  }
175366
+ function pauseListGoesBack(out) {
175367
+ const before = out.extra?.before;
175368
+ if (!Array.isArray(before) || !plainBody({ windows: before }) || pauseListShapeRefusal(before) !== null) return false;
175369
+ if (typeof out.beforeText !== "string") return true;
175370
+ const held = heldPauseWindows(out.beforeText, out.target ?? "pause-windows.json");
175371
+ if (held.problem) return false;
175372
+ try {
175373
+ parsePauseWindows(pauseWindowsText(held.doc, before), out.target ?? "pause-windows.json");
175374
+ return true;
175375
+ } catch {
175376
+ return false;
175377
+ }
175378
+ }
175253
175379
  function triggerEditBack(out) {
175254
175380
  const p = out.plan ?? {};
175255
175381
  const n3 = atIndex("trigger", p.index);
@@ -175344,7 +175470,7 @@ function adoptable(facts7, { name: name2, version }) {
175344
175470
  function actionNamed(name2) {
175345
175471
  return ACTIONS3.find((a) => a.name === name2) ?? null;
175346
175472
  }
175347
- var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
175473
+ var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, BODY_DEPTH_MAX, BODY_KEYS_MAX, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
175348
175474
  var init_actions = __esm({
175349
175475
  "src/console/actions.mjs"() {
175350
175476
  init_package_canary_record();
@@ -175381,6 +175507,8 @@ var init_actions = __esm({
175381
175507
  BULK_MAX2 = 100;
175382
175508
  CANCEL_TIMEOUT_SENTENCE = "no worker acknowledged: the job may have just finished, gone back to the queue (cancel it again to remove it), or its host is unreachable; nothing was changed";
175383
175509
  JOB_ID_RE2 = /^[A-Za-z0-9._:-]{1,200}$/;
175510
+ BODY_DEPTH_MAX = 8;
175511
+ BODY_KEYS_MAX = 64;
175384
175512
  sha2563 = (text4) => createHash28("sha256").update(text4).digest("hex");
175385
175513
  digestOf = (text4) => text4 === null || text4 === void 0 ? null : sha2563(text4);
175386
175514
  codeOf4 = (e2) => typeof e2?.code === "string" && /^[A-Z0-9_]{1,40}$/.test(e2.code) ? e2.code : "error";
@@ -175411,15 +175539,8 @@ var init_actions = __esm({
175411
175539
  needs: [],
175412
175540
  async preview(body, ctx) {
175413
175541
  if (!Array.isArray(body.windows)) return { refusal: 'send {"windows": [...]} with the whole new list of pause windows' };
175414
- for (const [i2, w] of body.windows.entries()) {
175415
- const at2 = atIndex("pause window", i2);
175416
- if (w === null || typeof w !== "object" || Array.isArray(w)) return { refusal: `${at2} is not an object; nothing was changed` };
175417
- for (const [k, value] of Object.entries(w)) {
175418
- if (!PAUSE_WINDOW_FIELDS.includes(k)) return { refusal: `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only); nothing was changed` };
175419
- const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
175420
- if (!plain) return { refusal: `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}; nothing was changed` };
175421
- }
175422
- }
175542
+ const shape2 = pauseListShapeRefusal(body.windows);
175543
+ if (shape2) return { refusal: `${shape2}; nothing was changed` };
175423
175544
  const path6 = ctx.config.pauseWindowsPath;
175424
175545
  const snap = readNow(ctx.fs, path6);
175425
175546
  if (snap.refusal) return snap;
@@ -176097,6 +176218,8 @@ var init_actions = __esm({
176097
176218
  envelopeAfter: "Set it back with the envelope form: the console kept no copy of the old values.",
176098
176219
  tooBig: "Put the previous list back by hand: this preview showed it; the console keeps no copy.",
176099
176220
  tooBigAfter: "Put the previous list back by hand: the console kept no copy of it.",
176221
+ refusedList: "Put the previous list back by hand: the console would refuse it as an Undo (a window it does not take, or more than 256 windows or days); this preview showed it, and the console keeps no copy.",
176222
+ refusedListAfter: "Put the previous list back by hand: the console would refuse it as an Undo (a window it does not take, or more than 256 windows or days), and kept no copy of it.",
176100
176223
  resumeUnread: "Pause the queues that were paused again by hand: the fleet was not read before the resume, so which they were is not known.",
176101
176224
  resumeShort: "Pause the queues that were paused again by hand: not every queue was read before the resume, so which they were is not fully known."
176102
176225
  });
@@ -178059,8 +178182,8 @@ var init_setup_wizard = __esm({
178059
178182
  init_connection();
178060
178183
  init_service_env();
178061
178184
  init_index();
178062
- RUNTIME_VERSION = "5.1.0";
178063
- RECEIVER_VERSION = "5.1.0";
178185
+ RUNTIME_VERSION = "5.2.0";
178186
+ RECEIVER_VERSION = "5.2.0";
178064
178187
  RUNTIME_PKG = "@edgehero/pi-dispatch";
178065
178188
  RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
178066
178189
  NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";