@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.
- package/dist/console.mjs +520 -398
- package/dist/index.mjs +512 -389
- 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
|
|
15041
|
-
|
|
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
|
-
|
|
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
|
|
162034
|
-
|
|
162035
|
-
const
|
|
162036
|
-
|
|
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 (
|
|
162050
|
-
if (staged.length === 0) return
|
|
162051
|
-
const running = staged.find((version) => brought(value, version) !== null);
|
|
162052
|
-
if (running === void 0) return
|
|
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
|
-
|
|
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
|
-
|
|
175415
|
-
|
|
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.
|
|
178063
|
-
RECEIVER_VERSION = "5.
|
|
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";
|