@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/console.mjs
CHANGED
|
@@ -5744,6 +5744,7 @@ var connection_exports = {};
|
|
|
5744
5744
|
__export(connection_exports, {
|
|
5745
5745
|
DB_REFUSED: () => DB_REFUSED,
|
|
5746
5746
|
JudgedConnector: () => JudgedConnector,
|
|
5747
|
+
VALKEY_COMMAND_TIMEOUT_MS: () => VALKEY_COMMAND_TIMEOUT_MS,
|
|
5747
5748
|
VALKEY_CONTEXT_KEYS: () => VALKEY_CONTEXT_KEYS,
|
|
5748
5749
|
VALKEY_ERROR_QUIET_MS: () => VALKEY_ERROR_QUIET_MS,
|
|
5749
5750
|
assertJudgedConnection: () => assertJudgedConnection,
|
|
@@ -5951,7 +5952,7 @@ async function claimValkeyOwner(url, folder2, { context = null, waitMs = 15e3, m
|
|
|
5951
5952
|
await sleep(500);
|
|
5952
5953
|
}
|
|
5953
5954
|
}
|
|
5954
|
-
var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, BoundedRedis, JUDGE, JudgedConnector;
|
|
5955
|
+
var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, BoundedRedis, VALKEY_COMMAND_TIMEOUT_MS, JUDGE, JudgedConnector;
|
|
5955
5956
|
var init_connection = __esm({
|
|
5956
5957
|
"../worker/src/connection.mjs"() {
|
|
5957
5958
|
init_valkey_auth();
|
|
@@ -6022,6 +6023,7 @@ var init_connection = __esm({
|
|
|
6022
6023
|
return answer2;
|
|
6023
6024
|
}
|
|
6024
6025
|
};
|
|
6026
|
+
VALKEY_COMMAND_TIMEOUT_MS = 5e3;
|
|
6025
6027
|
JUDGE = "piDispatchValkeyJudge";
|
|
6026
6028
|
JudgedConnector = class _JudgedConnector extends AbstractConnector {
|
|
6027
6029
|
constructor(options) {
|
|
@@ -6313,8 +6315,9 @@ function pauseWindowsVersionRefusal(doc, { supported = PAUSE_WINDOWS_VERSION } =
|
|
|
6313
6315
|
function heldPauseWindows(text4, path) {
|
|
6314
6316
|
if ((text4 ?? "").trim() === "") return { doc: null, windows: [] };
|
|
6315
6317
|
const { value, problem: problem2 } = heldDocument(text4, "windows");
|
|
6316
|
-
const
|
|
6317
|
-
|
|
6318
|
+
const doubled = value !== void 0 && findDuplicateKey(text4) !== null;
|
|
6319
|
+
const refused5 = doubled ? "has a key written twice" : problem2 ?? null;
|
|
6320
|
+
if (refused5) return { doc: doubled ? null : value ?? null, problem: `the pause-windows file ${path} ${refused5}, so its windows cannot be kept` };
|
|
6318
6321
|
return { doc: value, windows: Array.isArray(value.windows) ? value.windows : [] };
|
|
6319
6322
|
}
|
|
6320
6323
|
function pauseWindowsText(doc, windows, { table: table3 = PAUSE_WINDOW_FIELD_VERSIONS, valueTable = PAUSE_WINDOW_VALUE_VERSIONS } = {}) {
|
|
@@ -6365,6 +6368,10 @@ function parsePauseWindows(text4, path, { supported = PAUSE_WINDOWS_VERSION, tab
|
|
|
6365
6368
|
} catch (error) {
|
|
6366
6369
|
throw configError4(`pause-windows file is not valid JSON: ${path} (${error.message})`);
|
|
6367
6370
|
}
|
|
6371
|
+
const duplicate = findDuplicateKey(text4);
|
|
6372
|
+
if (duplicate) {
|
|
6373
|
+
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: ${path}`);
|
|
6374
|
+
}
|
|
6368
6375
|
const problem2 = pauseWindowsVersionRefusal(parsed, { supported });
|
|
6369
6376
|
if (problem2) {
|
|
6370
6377
|
const error = configError4(`${problem2.message}: ${path}`);
|
|
@@ -17584,10 +17591,12 @@ function withdrawCancel({ redis, jobId, interventionOrigin = "human" }) {
|
|
|
17584
17591
|
Promise.resolve().then(() => redis.del(cancelRequestKeyOf(jobId, interventionOrigin))).catch(() => {
|
|
17585
17592
|
});
|
|
17586
17593
|
}
|
|
17587
|
-
async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep = (ms3) => new Promise((r) => setTimeout(r, ms3)) }) {
|
|
17594
|
+
async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep = (ms3) => new Promise((r) => setTimeout(r, ms3)), now = Date.now, onWritten = null }) {
|
|
17588
17595
|
const requestKey = cancelRequestKeyOf(jobId, interventionOrigin);
|
|
17589
17596
|
const ackKey = interventionOrigin === "agent" ? agentCancelAckKey(jobId) : cancelAckKey(jobId);
|
|
17590
17597
|
await redis.set(requestKey, interventionOrigin === "agent" ? "agent-cancel" : "operator-cancel", "PX", CANCEL_REQ_TTL_MS);
|
|
17598
|
+
onWritten?.();
|
|
17599
|
+
const deadline = now() + ackTimeoutMs;
|
|
17591
17600
|
for (let waited = 0; ; waited += pollMs) {
|
|
17592
17601
|
let host;
|
|
17593
17602
|
try {
|
|
@@ -17597,7 +17606,7 @@ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTi
|
|
|
17597
17606
|
throw err;
|
|
17598
17607
|
}
|
|
17599
17608
|
if (host !== null && host !== void 0) return { ack: host };
|
|
17600
|
-
if (waited + pollMs > ackTimeoutMs) break;
|
|
17609
|
+
if (waited + pollMs > ackTimeoutMs || now() >= deadline) break;
|
|
17601
17610
|
await sleep(pollMs);
|
|
17602
17611
|
}
|
|
17603
17612
|
try {
|
|
@@ -22240,7 +22249,7 @@ function triggerLastRun(records, hostLoads = null) {
|
|
|
22240
22249
|
...sourcesOf(loaded, "package").filter((row2) => row2.toolNames?.length > 0).map((row2) => ({ kind: "package", from: row2.name ?? "a staged package", names: [...row2.toolNames] })),
|
|
22241
22250
|
...(overlay?.namedTools ?? []).map((row2) => ({ kind: "overlay", from: row2.name, names: [...row2.tools] }))
|
|
22242
22251
|
];
|
|
22243
|
-
const sources = [...sourcesOf(loaded, "package"), ...(overlay?.names ?? []).map((name3) => ({ name: name3, version: null, tools: overlay.namedTools?.find((row2) => row2.name === name3)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
|
|
22252
|
+
const sources = [...sourcesOf(loaded, "package").map((row2) => ({ ...row2, kind: "package" })), ...(overlay?.names ?? []).map((name3) => ({ name: name3, version: null, kind: "overlay", tools: overlay.namedTools?.find((row2) => row2.name === name3)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
|
|
22244
22253
|
return {
|
|
22245
22254
|
newest,
|
|
22246
22255
|
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] : []] }
|
|
@@ -22276,12 +22285,12 @@ function hostLoadsRow(hostLoads, name3) {
|
|
|
22276
22285
|
const row2 = Array.isArray(hostLoads?.hosts) ? hostLoads.hosts.find((entry3) => entry3?.name === name3) : null;
|
|
22277
22286
|
return row2 ?? { name: name3, state: "unknown", why: hostLoads?.unreachable ? "unreachable" : "absent" };
|
|
22278
22287
|
}
|
|
22279
|
-
function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null }) {
|
|
22288
|
+
function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null, mcp = null }) {
|
|
22280
22289
|
const row2 = hostLoadsRow(hostLoads, host);
|
|
22281
22290
|
const first = noOverlay(hostPiFor(row2, image));
|
|
22282
22291
|
const unknown4 = (why) => [`${first} What a job of trigger ${triggerIndex + 1} would load on this host is unknown: ${why}.`];
|
|
22283
22292
|
if (row2.state === "unknown") return unknown4(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");
|
|
22284
|
-
const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed));
|
|
22293
|
+
const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed) && (part !== "mcp" || !Array.isArray(mcp) || mcp.length > 0));
|
|
22285
22294
|
if (unread2.length > 0) return unknown4(`its last doctor run could not read ${listed(unread2.map((part) => UNREAD_PART_WORDS[part] ?? "part of what it publishes"))}`);
|
|
22286
22295
|
const packages = withholdsPackages ? [] : (row2.packages ?? []).filter((entry3) => !excludes.includes(entry3.name)).map((entry3) => `${entry3.name} ${entry3.version}`);
|
|
22287
22296
|
const extensions = row2.overlayExtensionsAllowed ? (row2.extensions ?? []).map((entry3) => entry3.name).filter((name3) => !excludes.some((excluded) => overlayEntryNamed(excluded, name3))) : [];
|
|
@@ -24710,6 +24719,8 @@ async function enqueueTriggerFire(plan, options = {}) {
|
|
|
24710
24719
|
const canary = options.packageCanary ?? null;
|
|
24711
24720
|
const id6 = (canary ? manualCanaryJobId : manualTriggerJobId)({ triggerId: current.schedulerId, now: instant9 });
|
|
24712
24721
|
redis = redisFn(url);
|
|
24722
|
+
redis.on?.("error", () => {
|
|
24723
|
+
});
|
|
24713
24724
|
if (await redis.set(`trigger-fire:${current.queue}:${id6}`, "1", "EX", 120, "NX") !== "OK") return { refusal: "This cron trigger was already fired this minute. Nothing was queued.", deduplicated: true };
|
|
24714
24725
|
queue = queueFn(parseConnectionFn(url, { failFast: true }), { name: current.queue });
|
|
24715
24726
|
const schedule = canary ? { ...current.schedule, data: withJobDataVersion({ ...current.schedule.data, packageCanary: canary }) } : current.schedule;
|
|
@@ -27204,6 +27215,8 @@ async function checkDispatchRun({
|
|
|
27204
27215
|
let redis;
|
|
27205
27216
|
try {
|
|
27206
27217
|
redis = redisFn(valkeyUrl);
|
|
27218
|
+
redis.on?.("error", () => {
|
|
27219
|
+
});
|
|
27207
27220
|
const key2 = hourKey(now());
|
|
27208
27221
|
const count8 = Number(consumeRate ? await redis.incr(key2) : await redis.get(key2) ?? 0);
|
|
27209
27222
|
if (consumeRate && count8 === 1) await redis.expire(key2, DISPATCH_RUN_TTL_SECONDS);
|
|
@@ -28077,6 +28090,8 @@ async function readHosts({ url, redisFn = makeRedisClient, timeoutMs = 2500, now
|
|
|
28077
28090
|
try {
|
|
28078
28091
|
parseConnection(url, { failFast: true });
|
|
28079
28092
|
redis = redisFn(url);
|
|
28093
|
+
redis.on?.("error", () => {
|
|
28094
|
+
});
|
|
28080
28095
|
const res = await withTimeout(readLiveHosts(redis, { now, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) }), timeoutMs, { unreachable: "timed out reaching the registry" });
|
|
28081
28096
|
if (res.unreachable) return { unreachable: res.unreachable };
|
|
28082
28097
|
return { hosts: res.hosts };
|
|
@@ -28100,6 +28115,8 @@ async function readHeldJobs({ url, limit = 20, redisFn = makeRedisClient, timeou
|
|
|
28100
28115
|
try {
|
|
28101
28116
|
parseConnection(url, { failFast: true });
|
|
28102
28117
|
redis = redisFn(url);
|
|
28118
|
+
redis.on?.("error", () => {
|
|
28119
|
+
});
|
|
28103
28120
|
const settled = (async () => {
|
|
28104
28121
|
const ids = await redis.smembers(HELD_SET);
|
|
28105
28122
|
const hydrated = await Promise.all(
|
|
@@ -28168,6 +28185,8 @@ async function cancelHeldJob({ url, jobId, redisFn = makeRedisClient, queueFn =
|
|
|
28168
28185
|
try {
|
|
28169
28186
|
parseConnection(url, { failFast: true });
|
|
28170
28187
|
redis = redisFn(url);
|
|
28188
|
+
redis.on?.("error", () => {
|
|
28189
|
+
});
|
|
28171
28190
|
settled = (async () => {
|
|
28172
28191
|
const { names: names2 } = await readFleetQueues({ url, redisFn, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) });
|
|
28173
28192
|
let owner2 = null;
|
|
@@ -28300,6 +28319,8 @@ async function readBudget({ url, redisFn = makeRedisClient, timeoutMs = 2500, no
|
|
|
28300
28319
|
try {
|
|
28301
28320
|
parseConnection(url, { failFast: true });
|
|
28302
28321
|
redis = redisFn(url);
|
|
28322
|
+
redis.on?.("error", () => {
|
|
28323
|
+
});
|
|
28303
28324
|
const settled = Promise.all([redis.get(dayKey(now)), redis.get(weekKey(now)), redis.get(monthKey(now)), redis.get(tokenDayKey(now))]).then(
|
|
28304
28325
|
([day2, week, month, tokens]) => ({ day: Number(day2 ?? 0), week: Number(week ?? 0), month: Number(month ?? 0), tokensToday: Number(tokens ?? 0) }),
|
|
28305
28326
|
(err) => ({ unreachable: err?.message ?? String(err) })
|
|
@@ -36769,371 +36790,6 @@ var init_loaded_parts = __esm({
|
|
|
36769
36790
|
}
|
|
36770
36791
|
});
|
|
36771
36792
|
|
|
36772
|
-
// src/console/panels/sandboxes.mjs
|
|
36773
|
-
function state(v) {
|
|
36774
|
-
const s = report(v)?.request;
|
|
36775
|
-
if (!s) return null;
|
|
36776
|
-
const success = s.state === "ready";
|
|
36777
|
-
return banner({ status: success ? "ok" : ["refused", "failed"].includes(s.state) ? "attention" : "running", title: success ? `Sandbox ${s.result?.id ?? s.id} is ready` : `Sandbox request ${s.state}`, body: html`<span class="code">${s.id}</span> on ${s.host ?? "its host"}. ${s.message ?? s.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s.result ? links(v, { ...s.result, id: s.result.id, host: s.result.host ?? s.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s.id, host: s.host ?? selected2(v).host ?? "" })}`, label: "Refresh request" }) });
|
|
36778
|
-
}
|
|
36779
|
-
function sandboxFromRun(v) {
|
|
36780
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
36781
|
-
const all = rows(v);
|
|
36782
|
-
const id6 = v.query?.id;
|
|
36783
|
-
const picked = all.find((r) => rowId(r) === id6 && (!v.query?.host || r.host === v.query.host));
|
|
36784
|
-
const pickerHost = v.query?.host ?? selected2(v).host ?? all[0]?.host ?? "";
|
|
36785
|
-
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((h) => ({ value: h.name, label: h.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id6 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r) => !pickerHost || r.host === pickerHost).map((r) => ({ value: rowId(r), label: `${rowId(r)} on ${r.host}${r.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
|
|
36786
|
-
if (!report(v)) return card2("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
|
|
36787
|
-
if (!picked) return card2("From a run", html`${state(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");
|
|
36788
|
-
const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
|
|
36789
|
-
const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
|
|
36790
|
-
const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
|
|
36791
|
-
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 });
|
|
36792
|
-
return card2("From a run", html`${state(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">${rowId(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");
|
|
36793
|
-
}
|
|
36794
|
-
function sandboxNew(v) {
|
|
36795
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
36796
|
-
const s = selected2(v), data5 = report(v), options = data5?.options ?? [];
|
|
36797
|
-
const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
|
|
36798
|
-
const triggers2 = options;
|
|
36799
|
-
const hostOptions = (data5?.hosts ?? []).filter((h) => !h.unreadable && !h.stale && h.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h.name))).map((h) => ({ value: h.name, label: `${h.name}, ${h.settings.defaultBackend ?? "venue not published"}` }));
|
|
36800
|
-
const fields2 = [
|
|
36801
|
-
field2({ label: "Repository", id: "sb-repo", control: input2({ id: "sb-repo", name: "repo", value: s.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
|
|
36802
|
-
field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s.index === void 0 ? "" : String(s.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." }),
|
|
36803
|
-
field2({ label: "Start from", id: "sb-ref", control: input2({ id: "sb-ref", name: "ref", value: s.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." }),
|
|
36804
|
-
field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
|
|
36805
|
-
field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
|
|
36806
|
-
];
|
|
36807
|
-
const plan = data5?.plan;
|
|
36808
|
-
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;
|
|
36809
|
-
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">${fields2}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
|
|
36810
|
-
const fresh = plan ? html`${facts([[`On ${plan.host}`, wordsHtml(sandboxFreshLine({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s.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;
|
|
36811
|
-
const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts4(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", s.repo], ["index", s.index, "int"], ["ref", s.ref], ["host", s.host], ["pin", Boolean(s.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", s.repo, "--ref", s.ref, "--as", String(s.index), ...s.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s.index + 1} is index ${s.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>`;
|
|
36812
|
-
return card2("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");
|
|
36813
|
-
}
|
|
36814
|
-
function sandboxOpen(v) {
|
|
36815
|
-
if (!allowed3(v)) return null;
|
|
36816
|
-
const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s) => !v.query?.gatewaySelect || s.host === v.query.host);
|
|
36817
|
-
const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
|
|
36818
|
-
const notices = (data5?.notices ?? []).filter((n2) => !v.query?.gatewaySelect || n2.host === v.query.host).map((n2) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n2.host}, ${n2.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." }[n2.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n2.host })}`}">Inspect host</a>.</p>`);
|
|
36819
|
-
if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card2("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>`);
|
|
36820
|
-
return card2("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((s) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s.id}</span>${vtag(s.venue)}</p><p class="sx-open__s">${s.host}, opened by ${s.by ?? "the terminal operator"}. Open since ${stamp(s.since)}.</p>${sandboxDriverLine(v, s)}<div class="sx-open__a">${links(v, s, "attach", true)}${sandboxBrowserLink(v, s)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s.host], ["id", s.id], ["nonce", s.nonce], ["containerId", s.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"}`));
|
|
36821
|
-
}
|
|
36822
|
-
function sandboxKept(v) {
|
|
36823
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
36824
|
-
const data5 = report(v), all = rows(v);
|
|
36825
|
-
return card2("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((h) => h.stale || h.unreadable).map((h) => html`<p class="sx-p" role="status">${h.name}: ${h.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r) => html`<li class="sx-kept${canOpen(r) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId(r), host: r.host })}`}">${rowId(r)}</a>${r.project ? ptag(r.project) : null}${r.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r.host} ${vtag(r.v ?? r.backend)}. ${retention(r, v.now)}${!canOpen(r) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r) ? html`<div class="sx-kept__actions">${links(v, r, "open", true)}${pinForm(v, r)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h) => h.more > 0).map((h) => html`<p class="sx-p">${h.name} has at least ${h.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
|
|
36826
|
-
}
|
|
36827
|
-
function sandboxBrowser(v) {
|
|
36828
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
36829
|
-
const gateway = v.data?.browserGateways;
|
|
36830
|
-
const count8 = gateway?.enabledHosts?.length ?? 0;
|
|
36831
|
-
return card2("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">${count8 ? `${count8} ${count8 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count8 ? 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(count8 ? "Enabled per host" : "Off by default", { status: count8 ? "ok" : "idle", icon: "shield" }));
|
|
36832
|
-
}
|
|
36833
|
-
var SANDBOX_WORDS, report, allowed3, acts2, rows, card2, stamp, rowId, canOpen, selected2, facts4, links, retention, pinForm, SANDBOX_PANELS;
|
|
36834
|
-
var init_sandboxes = __esm({
|
|
36835
|
-
"src/console/panels/sandboxes.mjs"() {
|
|
36836
|
-
init_html();
|
|
36837
|
-
init_kit();
|
|
36838
|
-
init_forges();
|
|
36839
|
-
init_sandbox_links();
|
|
36840
|
-
init_common();
|
|
36841
|
-
init_loaded_words();
|
|
36842
|
-
init_loaded_parts();
|
|
36843
|
-
SANDBOX_WORDS = Object.freeze({
|
|
36844
|
-
unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
|
|
36845
|
-
empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
|
|
36846
|
-
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.",
|
|
36847
|
-
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.",
|
|
36848
|
-
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."
|
|
36849
|
-
});
|
|
36850
|
-
report = (v) => v.data?.sandboxes;
|
|
36851
|
-
allowed3 = (v) => v.role === "operator";
|
|
36852
|
-
acts2 = (v) => allowed3(v) && !v.readOnly;
|
|
36853
|
-
rows = (v) => (report(v)?.hosts ?? []).flatMap((h) => (h.rows ?? []).map((r) => ({ ...r, host: h.name, hostStale: h.stale, retentionHours: h.retentionHours })));
|
|
36854
|
-
card2 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
|
|
36855
|
-
stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
|
|
36856
|
-
rowId = (r) => r.id ?? r.jobId;
|
|
36857
|
-
canOpen = (r) => !String(rowId(r)).startsWith("sha256:") && r.canOpen !== false && ["local", "podman"].includes(r.v ?? r.backend);
|
|
36858
|
-
selected2 = (v) => report(v)?.selected ?? {};
|
|
36859
|
-
facts4 = (plan) => facts([
|
|
36860
|
-
["Image", plan.image ?? "not published"],
|
|
36861
|
-
["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
|
|
36862
|
-
["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
|
|
36863
|
-
["Credentials", "None in the shell"]
|
|
36864
|
-
]);
|
|
36865
|
-
links = (v, r, verb = "open", compact2 = false) => sandboxLinks({ id: rowId(r), host: r.host, verb, compact: compact2, config: v.data?.sandboxHosts });
|
|
36866
|
-
retention = (r, now) => html`${r.pin ? "Pinned" : "Kept"}${Number.isFinite(r.until) ? html` until ${stamp(r.until)}${r.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r.until - now)} left`}` : ": deadline not published"}`;
|
|
36867
|
-
pinForm = (v, r) => acts2(v) && canOpen(r) ? actForm({ action: "sandbox-pin", hidden: [["host", r.host], ["id", rowId(r)]], label: "Preview pin", icon: "clock" }) : null;
|
|
36868
|
-
SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
|
|
36869
|
-
}
|
|
36870
|
-
});
|
|
36871
|
-
|
|
36872
|
-
// src/zero-touch-view.mjs
|
|
36873
|
-
function breakdown(row2, repository = false) {
|
|
36874
|
-
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: number2(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number2(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number2(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) };
|
|
36875
|
-
if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r) => breakdown(r, true)) : [];
|
|
36876
|
-
return out;
|
|
36877
|
-
}
|
|
36878
|
-
function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
|
|
36879
|
-
const observed = raw?.coverage ?? raw?.window;
|
|
36880
|
-
const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number2(observed.fromMs) !== null && observed.fromMs >= (number2(raw?.nowMs) ?? nowMs2);
|
|
36881
|
-
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: [] } };
|
|
36882
|
-
const totals = raw.totals ?? raw.summary ?? {};
|
|
36883
|
-
const coverage2 = raw.coverage ?? raw.window ?? {};
|
|
36884
|
-
const out = {
|
|
36885
|
-
state: "ready",
|
|
36886
|
-
nowMs: number2(raw.nowMs) ?? nowMs2,
|
|
36887
|
-
windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
|
|
36888
|
-
coverage: { requestedFromMs: number2(coverage2.requestedFromMs), fromMs: number2(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
|
|
36889
|
-
totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number2(totals.rate), previousRate: number2(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number2(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number2(totals.medianWaitMs) ?? (number2(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 },
|
|
36890
|
-
projects: Array.isArray(raw.projects) ? raw.projects.map((r) => breakdown(r)) : [],
|
|
36891
|
-
causes: causes(raw.causes),
|
|
36892
|
-
trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r) => ({ atMs: number2(r?.atMs ?? r?.at), rate: number2(r?.rate), runs: count5(r?.runs), touched: count5(r?.touched), partial: r?.partial === true })) : [],
|
|
36893
|
-
waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r) => ({ runId: id5(r?.runId ?? r?.jobId), parkedAt: number2(r?.parkedAt) })).filter((r) => r.runId && r.parkedAt !== null) : [],
|
|
36894
|
-
touchedRunIds: runIds(raw.touchedRunIds),
|
|
36895
|
-
ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
|
|
36896
|
-
groups: [],
|
|
36897
|
-
fixes: [],
|
|
36898
|
-
fixesPartial: raw.fixesPartial === true,
|
|
36899
|
-
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((r) => ({ runId: id5(r?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r?.status) ? r.status : "source-unavailable" })).filter((r) => r.runId) : [] }
|
|
36900
|
-
};
|
|
36901
|
-
out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f) => ({ id: id5(f?.id), at: number2(f?.at), undoneAt: number2(f?.undoneAt), kind: ["instructions", "config"].includes(f?.kind) ? f.kind : "config", groupId: id5(f?.groupId), reason: typeof f?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f.reason) ? f.reason : null, triggerIndex: Number.isSafeInteger(f?.triggerIndex) && f.triggerIndex >= 0 ? f.triggerIndex : null, beforeCount: number2(f?.beforeCount), afterCount: number2(f?.afterCount), spanMs: number2(f?.spanMs), partial: f?.partial !== false, subsequentRuns: count5(f?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f?.status) ? f.status : "observing" })).filter((f) => f.id) : [];
|
|
36902
|
-
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: number2(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
|
|
36903
|
-
return out;
|
|
36904
|
-
}
|
|
36905
|
-
var CAUSES, id5, number2, count5, text3, scope, causes, runIds;
|
|
36906
|
-
var init_zero_touch_view = __esm({
|
|
36907
|
-
"src/zero-touch-view.mjs"() {
|
|
36908
|
-
CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
|
|
36909
|
-
id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
|
|
36910
|
-
number2 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
|
|
36911
|
-
count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
|
|
36912
|
-
text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
|
|
36913
|
-
scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
|
|
36914
|
-
causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
|
|
36915
|
-
runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
|
|
36916
|
-
}
|
|
36917
|
-
});
|
|
36918
|
-
|
|
36919
|
-
// src/console/panels/zero-touch.mjs
|
|
36920
|
-
function zeroTouchTabs(view) {
|
|
36921
|
-
const here = view?.page === "zero-touch";
|
|
36922
|
-
const sandboxes = view?.page === "sandboxes";
|
|
36923
|
-
return tabs([{ label: "All runs", href: "/runs", icon: "runs", current: !here && !sandboxes }, { label: "Zero touch", href: "/runs/zero-touch", icon: "flag", current: here }, ...view?.role === "operator" ? [{ label: "Sandboxes", href: "/runs/sandboxes", icon: "box", current: sandboxes }] : []], { label: "Runs views" });
|
|
36924
|
-
}
|
|
36925
|
-
function zeroTouchControls(view) {
|
|
36926
|
-
const m = model3(view);
|
|
36927
|
-
return seg([7, 30, 90].map((days) => ({ label: `${days} days`, href: `/runs/zero-touch?${new URLSearchParams({ days: String(days), ...view?.query?.project ? { project: view.query.project } : {}, ...view?.query?.scope ? { scope: view.query.scope } : {} })}`, on: days === m.windowDays })), { label: "Window" });
|
|
36928
|
-
}
|
|
36929
|
-
function delta(n2, previousRate = null) {
|
|
36930
|
-
if (!Number.isFinite(n2)) return html`<span class="zt-tr">Previous interval not covered</span>`;
|
|
36931
|
-
return html`<span class="${`zt-tr${n2 < 0 ? " tone-ok" : n2 > 0 ? " tone-attention" : ""}`}">${icon(n2 < 0 ? "chev" : n2 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n2 === 0 ? "no change" : `${n2 < 0 ? "down" : "up"} ${amount2(Math.abs(n2))} points`}</b>${previousRate === null ? null : html`<small>from ${pct(previousRate)}</small>`}</span>`;
|
|
36932
|
-
}
|
|
36933
|
-
function coverage(m) {
|
|
36934
|
-
return html`<p class="zt-coverage" role="status">${m.coverage.partial ? html`<b>Partial ${m.windowDays}-day window.</b> Covered from ${date(m.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m.windowDays}-day window.`}${m.coverage.reasons.length ? html` ${m.coverage.reasons.join("; ")}.` : null}</p>`;
|
|
36935
|
-
}
|
|
36936
|
-
function spark(m) {
|
|
36937
|
-
const rows3 = m.trend;
|
|
36938
|
-
if (!rows3.some((r) => Number.isFinite(r.rate) && !r.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
|
|
36939
|
-
const top = Math.max(32, ...rows3.map((r) => r.rate ?? 0));
|
|
36940
|
-
const x = (i) => rows3.length === 1 ? 200 : i * 200 / (rows3.length - 1);
|
|
36941
|
-
const y = (n2) => 56 - n2 / top * 56;
|
|
36942
|
-
const segments = [];
|
|
36943
|
-
let points = [];
|
|
36944
|
-
for (const [i, row2] of rows3.entries()) {
|
|
36945
|
-
if (row2.rate === null || row2.partial) {
|
|
36946
|
-
if (points.length) segments.push(points);
|
|
36947
|
-
points = [];
|
|
36948
|
-
} else points.push(`${svgNum(x(i), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
|
|
36949
|
-
}
|
|
36950
|
-
if (points.length) segments.push(points);
|
|
36951
|
-
const last = rows3[rows3.length - 1];
|
|
36952
|
-
const label2 = `Rolling ${m.windowDays}-day touch rate at weekly intervals: ${rows3.map((r) => r.partial || r.rate === null ? "not covered" : pct(r.rate)).join(", ")}. Goal under 5 percent.`;
|
|
36953
|
-
return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label2}"><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">${label2}</p>`;
|
|
36954
|
-
}
|
|
36955
|
-
function longest(m) {
|
|
36956
|
-
return [...m.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
|
|
36957
|
-
}
|
|
36958
|
-
function waitingHistory(m) {
|
|
36959
|
-
return m.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;
|
|
36960
|
-
}
|
|
36961
|
-
function compact(m) {
|
|
36962
|
-
const best = longest(m);
|
|
36963
|
-
return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount2(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m.totals.waitingPartial ? "Not known" : `${amount2(m.totals.waitingHours)}h, median ${span(m.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m.totals.draftAnswers} ${m.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m.totals.waitingPartial ? "Not known" : `${m.totals.pending} ${m.totals.pending === 1 ? "run" : "runs"}`}</b>${m.waitingRuns.slice(0, 3).map((r) => html`<span>${link(r.runId)}, ${span(m.nowMs - r.parkedAt)}</span>`)}</li></ul>${waitingHistory(m)}`;
|
|
36964
|
-
}
|
|
36965
|
-
function zeroTouchRate(view) {
|
|
36966
|
-
const m = model3(view);
|
|
36967
|
-
return card({ title: `Touch rate, ${m.windowDays} days`, body: html`${fail4(m)}${m.state === "ready" ? html`${kpi(m.totals.runs ? pct(m.totals.rate) : "No runs", `${m.totals.touched} of ${m.totals.runs} runs needed a person`)}${spark(m)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m.totals.deltaPoints, m.totals.previousRate)}</p>${coverage(m)}<p class="zt-kps">${m.totals.stopped} stopped before they ran, outside the rate.${m.totals.stoppedUnattributed > 0 ? html` ${m.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m)}</div>` : null}` });
|
|
36968
|
-
}
|
|
36969
|
-
function zeroTouchStreak(view) {
|
|
36970
|
-
const m = model3(view), best = longest(m);
|
|
36971
|
-
return card({ title: "Zero-touch streak", body: html`${fail4(m)}${kpi(best ? `${amount2(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">${m.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" : `${amount2(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
|
|
36972
|
-
}
|
|
36973
|
-
function zeroTouchWait(view) {
|
|
36974
|
-
const m = model3(view);
|
|
36975
|
-
if (m.state !== "ready") return card({ title: "Waiting for a person", body: fail4(m) });
|
|
36976
|
-
return card({ title: "Waiting for a person", body: html`${fail4(m)}${kpi(m.totals.waitingPartial ? "Not known" : `${amount2(m.totals.waitingHours)}h`, `runs waited on a person in ${m.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m.totals.medianWaitMs)}</b></p>${waitingHistory(m)}<p class="zt-kp">${chip(m.totals.waitingPartial ? "Pending count not known" : `${m.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m.waitingRuns.slice(0, 3).map((r) => html`<span>${link(r.runId)}, ${span(m.nowMs - r.parkedAt)}</span>`)}</p>` });
|
|
36977
|
-
}
|
|
36978
|
-
function zeroTouchHandled(view) {
|
|
36979
|
-
const m = model3(view);
|
|
36980
|
-
return card({ title: "Answered from a draft", body: m.state !== "ready" ? fail4(m) : html`${kpi(m.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
|
|
36981
|
-
}
|
|
36982
|
-
function meter2(row2) {
|
|
36983
|
-
if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
|
|
36984
|
-
const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
36985
|
-
const word4 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
36986
|
-
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}: ${pct(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>${pct(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word4}</span></span></span>`;
|
|
36987
|
-
}
|
|
36988
|
-
function rowContent(row2, sub2 = false) {
|
|
36989
|
-
const c = causesByKey.get(row2.topCause);
|
|
36990
|
-
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((r) => r.name).join(" and ")}</small></span>${meter2(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" : `${amount2(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>`;
|
|
36991
|
-
}
|
|
36992
|
-
function phoneProjects(m, view) {
|
|
36993
|
-
const item2 = (row2, repository = false) => {
|
|
36994
|
-
const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
36995
|
-
const word4 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
36996
|
-
const query = new URLSearchParams({ days: String(m.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
|
|
36997
|
-
return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct(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>${pct(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word4}</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>`;
|
|
36998
|
-
};
|
|
36999
|
-
return html`<ul class="ztp-ps zt-phone-projects">${m.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r) => item2(r, true)) : null}`)}</ul>`;
|
|
37000
|
-
}
|
|
37001
|
-
function zeroTouchProjects(view) {
|
|
37002
|
-
const m = model3(view);
|
|
37003
|
-
if (m.state !== "ready") return card({ title: "By project", body: fail4(m) });
|
|
37004
|
-
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>`;
|
|
37005
|
-
const rows3 = m.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}: ${pct(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r) => html`<li class="zt-row zt-row--sub">${rowContent(r, 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(m.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
|
|
37006
|
-
return card({ title: "By project", sub: `${m.totals.touched} of ${m.totals.runs} runs needed a person`, flush: true, body: html`${fail4(m)}${m.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m, 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>`}` });
|
|
37007
|
-
}
|
|
37008
|
-
function causeCounts(values) {
|
|
37009
|
-
return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
|
|
37010
|
-
}
|
|
37011
|
-
function causeBar(values, max, name3) {
|
|
37012
|
-
const rows3 = causeCounts(values);
|
|
37013
|
-
let at2 = 0;
|
|
37014
|
-
max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
|
|
37015
|
-
const all = rows3.filter((r) => r.count > 0);
|
|
37016
|
-
const label2 = `${name3}: ${all.map((r) => `${r.label} ${r.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
|
|
37017
|
-
return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label2}">${all.map((r) => {
|
|
37018
|
-
const x = at2 / Math.max(1, max) * 500, w = r.count / Math.max(1, max) * 500;
|
|
37019
|
-
at2 += r.count;
|
|
37020
|
-
return html`<g class="${`tone-${r.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x / 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((x + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r.count}</text>` : null}</g>`;
|
|
37021
|
-
})}</svg>`;
|
|
37022
|
-
}
|
|
37023
|
-
function zeroTouchCauses(view) {
|
|
37024
|
-
const m = model3(view), max = Math.max(1, ...m.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
|
|
37025
|
-
if (m.state !== "ready") return card({ title: "What needed a person", body: fail4(m) });
|
|
37026
|
-
const bars = m.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>`);
|
|
37027
|
-
const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
|
|
37028
|
-
return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m.windowDays} days, by project</span><span class="zt-phone-label">${m.windowDays} days, ${scopeLabel}</span>`, body: html`${fail4(m)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m.causes, Math.max(1, m.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m.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">${m.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>` });
|
|
37029
|
-
}
|
|
37030
|
-
function guardedForm(action2, { hidden: hidden2 = [], fields: fields2 = null, label: label2, kind = ["sm"] } = {}) {
|
|
37031
|
-
return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name3, value, type]) => html`<input type="hidden" name="${name3}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields2}${btn({ type: "submit", kind, label: label2 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
|
|
37032
|
-
}
|
|
37033
|
-
function nameForm(g, index2) {
|
|
37034
|
-
const key2 = `zt-name-${index2}`;
|
|
37035
|
-
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: input2({ 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>`;
|
|
37036
|
-
}
|
|
37037
|
-
function instructionForm(g, index2) {
|
|
37038
|
-
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>`;
|
|
37039
|
-
const key2 = `zt-sentence-${index2}`;
|
|
37040
|
-
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>`;
|
|
37041
|
-
}
|
|
37042
|
-
function zeroTouchQuestions(view) {
|
|
37043
|
-
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." });
|
|
37044
|
-
const m = model3(view);
|
|
37045
|
-
if (m.state !== "ready") return card({ title: "Questions it keeps asking", body: fail4(m) });
|
|
37046
|
-
const rows3 = m.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" && m.fixes.some((f) => f.groupId === g.id && f.afterCount === 0 && f.subsequentRuns > 0 && !f.partial && f.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((id6, at2) => html`${at2 ? ", " : ""}${link(id6)}`)}${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>`);
|
|
37047
|
-
const phoneRows = m.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>`);
|
|
37048
|
-
const sources = m.grouping.sources.length ? m.grouping.sources : m.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
|
|
37049
|
-
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>${fail4(m)}${m.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(m.grouping.state) ? html`<p class="zt-note" role="status">${m.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(m.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>${m.grouping.enabled ? `${plural5(m.grouping.pending, "grouping job")} pending; ${plural5(m.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>` });
|
|
37050
|
-
}
|
|
37051
|
-
function fixTone(f) {
|
|
37052
|
-
if (f.partial || f.subsequentRuns === 0 || f.undoneAt !== null || f.status === "observing") return "idle";
|
|
37053
|
-
return f.status === "improved" ? "ok" : f.status === "worse" ? "failed" : "idle";
|
|
37054
|
-
}
|
|
37055
|
-
function fixObservation(f) {
|
|
37056
|
-
if (f.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
|
|
37057
|
-
if (f.partial) return "Partial observation: effectiveness is not established.";
|
|
37058
|
-
if (f.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
|
|
37059
|
-
return f.status === "improved" ? "Fewer occurrences over equal observed spans." : f.status === "worse" ? "More occurrences over equal observed spans." : f.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
|
|
37060
|
-
}
|
|
37061
|
-
function beforeAfter(label2, value, max, tone = "attention") {
|
|
37062
|
-
return html`<p class="zt-ba"><span>${label2}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label2}: ${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>`;
|
|
37063
|
-
}
|
|
37064
|
-
function zeroTouchFixes(view) {
|
|
37065
|
-
const m = model3(view);
|
|
37066
|
-
const evidence = html`${m.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}${m.fixes.length ? html`<ol class="zt-fxs">${m.fixes.map((f) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f.at)} UTC${f.undoneAt === null ? null : html` · Reverted ${date(f.undoneAt)} UTC`}</p><p class="zt-fx__w">${f.kind === "instructions" ? `Trigger ${f.triggerIndex === null ? "not known" : f.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f.reason ? `${f.reason} refusals` : "Questions in the targeted group"}, equal ${span(f.spanMs)} spans before and after; ${f.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f.beforeCount, Math.max(f.beforeCount ?? 0, f.afterCount ?? 0, 1))}${beforeAfter("After", f.afterCount, Math.max(f.beforeCount ?? 0, f.afterCount ?? 0, 1), fixTone(f))}<p class="zt-kps">${fixObservation(f)}</p>${acts3(view) && f.kind === "instructions" && f.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
|
|
37067
|
-
const brief = m.fixes.map((f) => `${f.reason ?? "grouped questions"}: ${f.beforeCount ?? "?"} to ${f.afterCount ?? "?"}`).join("; ");
|
|
37068
|
-
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>${m.fixesPartial ? "Fix history incomplete" : `${m.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
|
|
37069
|
-
}
|
|
37070
|
-
var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model3, acts3, pct, amount2, date, span, link, fail4, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
|
|
37071
|
-
var init_zero_touch = __esm({
|
|
37072
|
-
"src/console/panels/zero-touch.mjs"() {
|
|
37073
|
-
init_common();
|
|
37074
|
-
init_html();
|
|
37075
|
-
init_kit();
|
|
37076
|
-
init_zero_touch_view();
|
|
37077
|
-
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." });
|
|
37078
|
-
ZERO_TOUCH_CAUSES = Object.freeze([
|
|
37079
|
-
{ cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
|
|
37080
|
-
{ cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
|
|
37081
|
-
{ 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." },
|
|
37082
|
-
{ 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." },
|
|
37083
|
-
{ cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
|
|
37084
|
-
{ cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
|
|
37085
|
-
]);
|
|
37086
|
-
ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
|
|
37087
|
-
unassigned: "Not grouped yet.",
|
|
37088
|
-
pending: "A grouping job is pending.",
|
|
37089
|
-
assigned: "Assigned to a question group.",
|
|
37090
|
-
"source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
|
|
37091
|
-
"source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
|
|
37092
|
-
"source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
|
|
37093
|
-
"candidates-expired": "A group's representative context expired. Review the groups before trying again.",
|
|
37094
|
-
"group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
|
|
37095
|
-
stale: "The group revision changed during classification. Preview a fresh grouping job.",
|
|
37096
|
-
refused: "The grouping job was refused. Open its run to see the policy reason.",
|
|
37097
|
-
failed: "The grouping job failed. Open its run and review the failure before trying again."
|
|
37098
|
-
});
|
|
37099
|
-
causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
|
|
37100
|
-
model3 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
|
|
37101
|
-
acts3 = (v) => v?.role === "operator" && !v?.readOnly;
|
|
37102
|
-
pct = (n2) => Number.isFinite(n2) ? `${Math.round(n2)}%` : "Not known";
|
|
37103
|
-
amount2 = (n2) => Number.isFinite(n2) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n2) : "Not known";
|
|
37104
|
-
date = (n2) => Number.isFinite(n2) && n2 >= 0 && n2 <= 864e13 ? new Date(n2).toISOString().slice(0, 10) : "not known";
|
|
37105
|
-
span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount2(ms3 / 36e5)}h` : `${amount2(ms3 / 6e4)}m`;
|
|
37106
|
-
link = (id6) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id6)}`}">${id6}</a>`;
|
|
37107
|
-
fail4 = (m) => m.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
|
|
37108
|
-
ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
|
|
37109
|
-
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]]);
|
|
37110
|
-
}
|
|
37111
|
-
});
|
|
37112
|
-
|
|
37113
|
-
// src/trigger-policy.mjs
|
|
37114
|
-
function triggerWhoMayFire(trigger2) {
|
|
37115
|
-
const on = trigger2.on ?? trigger2;
|
|
37116
|
-
const forge = trigger2.run?.kind ?? trigger2.forge;
|
|
37117
|
-
const kind = on.type;
|
|
37118
|
-
if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
|
|
37119
|
-
if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
|
|
37120
|
-
if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
|
|
37121
|
-
if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
|
|
37122
|
-
if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
|
|
37123
|
-
if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
|
|
37124
|
-
const actions = Array.isArray(on.action) ? on.action : [];
|
|
37125
|
-
const gates = [];
|
|
37126
|
-
if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
|
|
37127
|
-
if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
|
|
37128
|
-
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");
|
|
37129
|
-
if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
|
|
37130
|
-
return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
|
|
37131
|
-
}
|
|
37132
|
-
var init_trigger_policy = __esm({
|
|
37133
|
-
"src/trigger-policy.mjs"() {
|
|
37134
|
-
}
|
|
37135
|
-
});
|
|
37136
|
-
|
|
37137
36793
|
// src/trigger-edit-core.mjs
|
|
37138
36794
|
import * as nodeFs26 from "node:fs";
|
|
37139
36795
|
import { createHash as createHash37 } from "node:crypto";
|
|
@@ -37333,6 +36989,7 @@ function flowAnswer(row2, trigger2) {
|
|
|
37333
36989
|
const found = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow);
|
|
37334
36990
|
if (!found) return null;
|
|
37335
36991
|
if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
|
|
36992
|
+
if (found.tier === "package" && trigger2.packages === false) return null;
|
|
37336
36993
|
return found.tier === "package" ? `from ${found.package ?? "a staged package"}` : FLOW_TIER_WORDS[found.tier] ?? null;
|
|
37337
36994
|
}
|
|
37338
36995
|
function lacksFlow(row2, trigger2) {
|
|
@@ -37472,7 +37129,7 @@ function overlayStem(segment) {
|
|
|
37472
37129
|
for (let end = 1; end < segment.length; end += 1) if (overlayEntryNamed(segment.slice(0, end), segment)) return segment.slice(0, end);
|
|
37473
37130
|
return segment;
|
|
37474
37131
|
}
|
|
37475
|
-
function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
37132
|
+
function extensionsRow2({ trigger: trigger2, rows: rows3, last, records = [], historyUnread = false, hostLoads = null }) {
|
|
37476
37133
|
const known2 = rows3.filter((row2) => row2.state === "current");
|
|
37477
37134
|
const inFile = trigger2.excludeExtensions ?? [];
|
|
37478
37135
|
const withheld = trigger2.packages === false;
|
|
@@ -37488,10 +37145,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
37488
37145
|
for (const value of naming.length > 0 ? naming : [overlayStem(segment)]) groupOf(value).segments.add(segment);
|
|
37489
37146
|
}
|
|
37490
37147
|
for (const name3 of inFile) groupOf(name3);
|
|
37491
|
-
const
|
|
37492
|
-
|
|
37493
|
-
const
|
|
37494
|
-
|
|
37148
|
+
const said = (Array.isArray(records) ? records : []).map((record3) => triggerLastRun([record3], hostLoads).quoted).filter(Boolean);
|
|
37149
|
+
const brought = (name3, { kind, version: version2 = null }) => {
|
|
37150
|
+
for (const run4 of [last, ...said].filter(Boolean)) {
|
|
37151
|
+
const row2 = (run4.sources ?? []).find((source2) => source2.kind === kind && source2.name === name3 && (version2 === null || source2.version === version2));
|
|
37152
|
+
if (row2) return `${broughtWords(row2) ?? "no tool, skill or prompt template"}, as ${run4.id} loaded it`;
|
|
37153
|
+
}
|
|
37154
|
+
return null;
|
|
37495
37155
|
};
|
|
37496
37156
|
const items = [...groups.values()].map(({ value, versions: versions2, segments }) => {
|
|
37497
37157
|
const on = !inFile.includes(value);
|
|
@@ -37503,12 +37163,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
37503
37163
|
const off = known2.filter((row2) => holdsEntry(row2) && row2.overlayExtensionsAllowed !== true).map((row2) => row2.name);
|
|
37504
37164
|
const hostsOn = (version2) => known2.filter((row2) => (row2.packages ?? []).some((entry3) => entry3.name === value && entry3.version === version2)).map((row2) => row2.name);
|
|
37505
37165
|
const stagedWhere = staged.length === 0 ? null : whereWords(rows3, "packages", holdsPackage, "staged", staged.length === 1 ? null : staged.map((version2) => `${version2} on ${listed(hostsOn(version2))}`).join(", "));
|
|
37166
|
+
const fromEntries = () => entries.map((segment) => brought(segment, { kind: "overlay" })).find((words) => words !== null) ?? null;
|
|
37506
37167
|
const quoted = (() => {
|
|
37507
|
-
if (
|
|
37508
|
-
if (staged.length === 0) return
|
|
37509
|
-
const running = staged.find((version2) => brought(value, version2) !== null);
|
|
37510
|
-
if (running === void 0) return
|
|
37511
|
-
return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, running)}`;
|
|
37168
|
+
if (withheld && entries.length === 0) return null;
|
|
37169
|
+
if (staged.length === 0 || withheld) return fromEntries();
|
|
37170
|
+
const running = staged.find((version2) => brought(value, { kind: "package", version: version2 }) !== null);
|
|
37171
|
+
if (running === void 0) return fromEntries();
|
|
37172
|
+
return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, { kind: "package", version: running })}`;
|
|
37512
37173
|
})();
|
|
37513
37174
|
const also = inFile.includes(value) ? alsoNaming(inFile, value) : [];
|
|
37514
37175
|
const named2 = staged.length > 0 ? `${value} ${listed(staged)}` : entries.length > 0 && also.length === 0 ? entries.join(", ") : value;
|
|
@@ -37518,7 +37179,9 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
37518
37179
|
entries.length > 0 ? whereWords(rows3, "extensions", holdsEntry, "in the overlay") : null,
|
|
37519
37180
|
off.length > 0 ? `its overlay extensions off on ${listed(off)}` : null,
|
|
37520
37181
|
also.length > 0 ? `the file also names ${listed(also)}` : null,
|
|
37521
|
-
quoted
|
|
37182
|
+
quoted,
|
|
37183
|
+
// Not said where the history could not be read: the card says so once, and a failed read is not "no run".
|
|
37184
|
+
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
|
|
37522
37185
|
].filter(Boolean).join(" \xB7 ");
|
|
37523
37186
|
return { value, name: named2, on, moot, sub: sub2, backOn: also.length > 0 ? `${value} off the list` : `${value} back on` };
|
|
37524
37187
|
});
|
|
@@ -37621,14 +37284,67 @@ function triggerGetsOf({ trigger: trigger2, records = [], historyUnread = false,
|
|
|
37621
37284
|
last: { Model: under("Model"), Skills: under("Skills"), Extensions: under("Extensions"), MCP: under("MCP"), Tools: under("Tools"), pi: under("pi") },
|
|
37622
37285
|
model: modelRow2({ trigger: trigger2, defaults: defaults2, modelChoices }),
|
|
37623
37286
|
flow: { ...flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }), ...watch ? { doctor: null, lacking: null } : {} },
|
|
37624
|
-
extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted }),
|
|
37287
|
+
extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted, records, historyUnread, hostLoads }),
|
|
37625
37288
|
mcp: mcpRow2({ trigger: trigger2, rows: rows3, overlayServers }),
|
|
37626
37289
|
tools: toolsRow2({ trigger: trigger2, last: quoted }),
|
|
37627
37290
|
image: imageRow({ trigger: trigger2, rows: rows3 }),
|
|
37628
37291
|
secrets: { profile: trigger2.secretsProfile ?? null, count: trigger2.secrets ?? 0, optedOut: trigger2.secretBundle === false }
|
|
37629
37292
|
};
|
|
37630
37293
|
}
|
|
37631
|
-
|
|
37294
|
+
function freshFlowWords(row2, trigger2) {
|
|
37295
|
+
if (trigger2.flow === null) return SANDBOX_FRESH_WORDS.noFlow;
|
|
37296
|
+
const who = `a job of trigger ${trigger2.index + 1} on this host`;
|
|
37297
|
+
const word4 = flowAnswer(row2, trigger2);
|
|
37298
|
+
if (word4 === null) return `Whether ${who} would find ${trigger2.flow} is unknown: its last doctor run has no answer for it.`;
|
|
37299
|
+
const tier = (row2.flows ?? []).find((entry3) => entry3.trigger === trigger2.index && entry3.flow === trigger2.flow)?.tier;
|
|
37300
|
+
if (tier === "none" || tier === "unknown") return `Whether ${who} would find ${trigger2.flow} is unknown: doctor found it ${word4}, and the repository, which is on no host, may hold it.`;
|
|
37301
|
+
if (tier === "repository") return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word4}.`;
|
|
37302
|
+
return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word4}, unless the repository holds it at its base commit, which comes first.`;
|
|
37303
|
+
}
|
|
37304
|
+
function freshMcpWords(row2, named2) {
|
|
37305
|
+
if (!Array.isArray(named2) || named2.length === 0) return null;
|
|
37306
|
+
const on = [];
|
|
37307
|
+
const off = [];
|
|
37308
|
+
const lacking = [];
|
|
37309
|
+
const unknown4 = [];
|
|
37310
|
+
let forge = false;
|
|
37311
|
+
for (const name3 of named2) {
|
|
37312
|
+
if (name3 === FORGE_MCP_SERVER_NAME) {
|
|
37313
|
+
forge = true;
|
|
37314
|
+
continue;
|
|
37315
|
+
}
|
|
37316
|
+
const server = (row2.mcp ?? []).find((entry3) => entry3.name === name3);
|
|
37317
|
+
if (server) (server.enabled === true ? on : off).push(name3);
|
|
37318
|
+
else (freshListsWhole(row2, "mcp") ? lacking : unknown4).push(name3);
|
|
37319
|
+
}
|
|
37320
|
+
const refused5 = [off.length > 0 ? `${listed(off)} switched off here` : null, lacking.length > 0 ? `${listed(lacking)} not on this host` : null].filter(Boolean);
|
|
37321
|
+
const said = [
|
|
37322
|
+
on.length > 0 ? `${listed(on)} on this host` : null,
|
|
37323
|
+
// The reason is not named: a host with no mcp.json publishes the same empty list, and its jobs are refused as
|
|
37324
|
+
// mcp-config-invalid rather than mcp-server-unknown (`serverRefused`). Either way, before it spends.
|
|
37325
|
+
refused5.length > 0 ? `${refused5.join(" and ")}, so a job there is refused before it spends` : null,
|
|
37326
|
+
unknown4.length > 0 ? `${listed(unknown4)} unknown here` : null,
|
|
37327
|
+
forge ? `${FORGE_MCP_SERVER_NAME}: ${TRIGGER_GETS_WORDS.forgeServer}` : null
|
|
37328
|
+
].filter(Boolean);
|
|
37329
|
+
return `${said.join("; ")}.`;
|
|
37330
|
+
}
|
|
37331
|
+
function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
|
|
37332
|
+
const rows3 = [[`On ${host}`, sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages, excludes, image, mcp })]];
|
|
37333
|
+
const row2 = hostLoadsRow(hostLoads, host);
|
|
37334
|
+
if (row2.state === "unknown") return rows3;
|
|
37335
|
+
const named2 = typeof flow === "string" && flow !== "" ? flow : null;
|
|
37336
|
+
if (row2.state !== "current") {
|
|
37337
|
+
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.`]]);
|
|
37338
|
+
const listedNames = (mcp ?? []).filter((name3) => typeof name3 === "string");
|
|
37339
|
+
if (listedNames.length > 0) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [`${listed(listedNames)} unknown here: its last doctor run is old.`]]);
|
|
37340
|
+
return rows3;
|
|
37341
|
+
}
|
|
37342
|
+
rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0 })]]);
|
|
37343
|
+
const mcpWords = freshMcpWords(row2, mcp);
|
|
37344
|
+
if (mcpWords !== null) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [mcpWords]]);
|
|
37345
|
+
return rows3;
|
|
37346
|
+
}
|
|
37347
|
+
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, plural6, alsoNaming, SANDBOX_FRESH_WORDS, freshListsWhole;
|
|
37632
37348
|
var init_trigger_gets_view = __esm({
|
|
37633
37349
|
"src/trigger-gets-view.mjs"() {
|
|
37634
37350
|
init_mcp_config();
|
|
@@ -37664,6 +37380,8 @@ var init_trigger_gets_view = __esm({
|
|
|
37664
37380
|
noExtensions: "The hosts that can say name no staged package or overlay extension.",
|
|
37665
37381
|
noExtensionsKnown: "No host\u2019s doctor run can say which extensions it has.",
|
|
37666
37382
|
forgeServer: "the job\u2019s own forge, with its own token; in no overlay",
|
|
37383
|
+
bringsUnknown: "what it brings is known once a run has loaded it",
|
|
37384
|
+
overlayBringsUnknown: "an overlay entry has no manifest, so what it brings is known once a run has loaded it",
|
|
37667
37385
|
noServers: "The hosts that can say name no MCP server, and this trigger names none.",
|
|
37668
37386
|
noServersKnown: "No host\u2019s doctor run can say which MCP servers it has, and this trigger names none.",
|
|
37669
37387
|
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.",
|
|
@@ -37709,6 +37427,378 @@ var init_trigger_gets_view = __esm({
|
|
|
37709
37427
|
listsWhole = (row2, part) => row2.state === "current" && !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
|
|
37710
37428
|
plural6 = (n2, one, many = `${one}s`) => `${n2} ${n2 === 1 ? one : many}`;
|
|
37711
37429
|
alsoNaming = (list4, name3) => list4.filter((other) => other !== name3 && (overlayEntryNamed(other, name3) || overlayEntryNamed(name3, other)));
|
|
37430
|
+
SANDBOX_FRESH_WORDS = Object.freeze({
|
|
37431
|
+
noFlow: "This trigger names no flow.",
|
|
37432
|
+
flowLabel: "Flow",
|
|
37433
|
+
mcpLabel: "MCP servers"
|
|
37434
|
+
});
|
|
37435
|
+
freshListsWhole = (row2, part) => !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
|
|
37436
|
+
}
|
|
37437
|
+
});
|
|
37438
|
+
|
|
37439
|
+
// src/console/panels/sandboxes.mjs
|
|
37440
|
+
function state(v) {
|
|
37441
|
+
const s = report(v)?.request;
|
|
37442
|
+
if (!s) return null;
|
|
37443
|
+
const success = s.state === "ready";
|
|
37444
|
+
return banner({ status: success ? "ok" : ["refused", "failed"].includes(s.state) ? "attention" : "running", title: success ? `Sandbox ${s.result?.id ?? s.id} is ready` : `Sandbox request ${s.state}`, body: html`<span class="code">${s.id}</span> on ${s.host ?? "its host"}. ${s.message ?? s.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s.result ? links(v, { ...s.result, id: s.result.id, host: s.result.host ?? s.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s.id, host: s.host ?? selected2(v).host ?? "" })}`, label: "Refresh request" }) });
|
|
37445
|
+
}
|
|
37446
|
+
function sandboxFromRun(v) {
|
|
37447
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
37448
|
+
const all = rows(v);
|
|
37449
|
+
const id6 = v.query?.id;
|
|
37450
|
+
const picked = all.find((r) => rowId(r) === id6 && (!v.query?.host || r.host === v.query.host));
|
|
37451
|
+
const pickerHost = v.query?.host ?? selected2(v).host ?? all[0]?.host ?? "";
|
|
37452
|
+
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((h) => ({ value: h.name, label: h.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id6 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r) => !pickerHost || r.host === pickerHost).map((r) => ({ value: rowId(r), label: `${rowId(r)} on ${r.host}${r.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
|
|
37453
|
+
if (!report(v)) return card2("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
|
|
37454
|
+
if (!picked) return card2("From a run", html`${state(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");
|
|
37455
|
+
const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
|
|
37456
|
+
const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
|
|
37457
|
+
const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
|
|
37458
|
+
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 });
|
|
37459
|
+
return card2("From a run", html`${state(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">${rowId(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");
|
|
37460
|
+
}
|
|
37461
|
+
function sandboxNew(v) {
|
|
37462
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
37463
|
+
const s = selected2(v), data5 = report(v), options = data5?.options ?? [];
|
|
37464
|
+
const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
|
|
37465
|
+
const triggers2 = options;
|
|
37466
|
+
const hostOptions = (data5?.hosts ?? []).filter((h) => !h.unreadable && !h.stale && h.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h.name))).map((h) => ({ value: h.name, label: `${h.name}, ${h.settings.defaultBackend ?? "venue not published"}` }));
|
|
37467
|
+
const fields2 = [
|
|
37468
|
+
field2({ label: "Repository", id: "sb-repo", control: input2({ id: "sb-repo", name: "repo", value: s.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
|
|
37469
|
+
field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s.index === void 0 ? "" : String(s.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." }),
|
|
37470
|
+
field2({ label: "Start from", id: "sb-ref", control: input2({ id: "sb-ref", name: "ref", value: s.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." }),
|
|
37471
|
+
field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
|
|
37472
|
+
field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
|
|
37473
|
+
];
|
|
37474
|
+
const plan = data5?.plan;
|
|
37475
|
+
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;
|
|
37476
|
+
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">${fields2}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
|
|
37477
|
+
const fresh = plan ? html`${facts(sandboxFreshRows({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s.index, flow: data5?.flow ?? null, mcp: data5?.mcp ?? [], withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }).map(([label2, parts2]) => [label2, wordsHtml(parts2)]), { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
|
|
37478
|
+
const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts4(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", s.repo], ["index", s.index, "int"], ["ref", s.ref], ["host", s.host], ["pin", Boolean(s.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", s.repo, "--ref", s.ref, "--as", String(s.index), ...s.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s.index + 1} is index ${s.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>`;
|
|
37479
|
+
return card2("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");
|
|
37480
|
+
}
|
|
37481
|
+
function sandboxOpen(v) {
|
|
37482
|
+
if (!allowed3(v)) return null;
|
|
37483
|
+
const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s) => !v.query?.gatewaySelect || s.host === v.query.host);
|
|
37484
|
+
const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
|
|
37485
|
+
const notices = (data5?.notices ?? []).filter((n2) => !v.query?.gatewaySelect || n2.host === v.query.host).map((n2) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n2.host}, ${n2.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." }[n2.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n2.host })}`}">Inspect host</a>.</p>`);
|
|
37486
|
+
if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card2("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>`);
|
|
37487
|
+
return card2("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((s) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s.id}</span>${vtag(s.venue)}</p><p class="sx-open__s">${s.host}, opened by ${s.by ?? "the terminal operator"}. Open since ${stamp(s.since)}.</p>${sandboxDriverLine(v, s)}<div class="sx-open__a">${links(v, s, "attach", true)}${sandboxBrowserLink(v, s)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s.host], ["id", s.id], ["nonce", s.nonce], ["containerId", s.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"}`));
|
|
37488
|
+
}
|
|
37489
|
+
function sandboxKept(v) {
|
|
37490
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
37491
|
+
const data5 = report(v), all = rows(v);
|
|
37492
|
+
return card2("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((h) => h.stale || h.unreadable).map((h) => html`<p class="sx-p" role="status">${h.name}: ${h.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r) => html`<li class="sx-kept${canOpen(r) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId(r), host: r.host })}`}">${rowId(r)}</a>${r.project ? ptag(r.project) : null}${r.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r.host} ${vtag(r.v ?? r.backend)}. ${retention(r, v.now)}${!canOpen(r) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r) ? html`<div class="sx-kept__actions">${links(v, r, "open", true)}${pinForm(v, r)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h) => h.more > 0).map((h) => html`<p class="sx-p">${h.name} has at least ${h.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
|
|
37493
|
+
}
|
|
37494
|
+
function sandboxBrowser(v) {
|
|
37495
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
37496
|
+
const gateway = v.data?.browserGateways;
|
|
37497
|
+
const count8 = gateway?.enabledHosts?.length ?? 0;
|
|
37498
|
+
return card2("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">${count8 ? `${count8} ${count8 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count8 ? 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(count8 ? "Enabled per host" : "Off by default", { status: count8 ? "ok" : "idle", icon: "shield" }));
|
|
37499
|
+
}
|
|
37500
|
+
var SANDBOX_WORDS, report, allowed3, acts2, rows, card2, stamp, rowId, canOpen, selected2, facts4, links, retention, pinForm, SANDBOX_PANELS;
|
|
37501
|
+
var init_sandboxes = __esm({
|
|
37502
|
+
"src/console/panels/sandboxes.mjs"() {
|
|
37503
|
+
init_html();
|
|
37504
|
+
init_kit();
|
|
37505
|
+
init_forges();
|
|
37506
|
+
init_sandbox_links();
|
|
37507
|
+
init_common();
|
|
37508
|
+
init_loaded_words();
|
|
37509
|
+
init_loaded_parts();
|
|
37510
|
+
init_trigger_gets_view();
|
|
37511
|
+
SANDBOX_WORDS = Object.freeze({
|
|
37512
|
+
unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
|
|
37513
|
+
empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
|
|
37514
|
+
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.",
|
|
37515
|
+
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.",
|
|
37516
|
+
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."
|
|
37517
|
+
});
|
|
37518
|
+
report = (v) => v.data?.sandboxes;
|
|
37519
|
+
allowed3 = (v) => v.role === "operator";
|
|
37520
|
+
acts2 = (v) => allowed3(v) && !v.readOnly;
|
|
37521
|
+
rows = (v) => (report(v)?.hosts ?? []).flatMap((h) => (h.rows ?? []).map((r) => ({ ...r, host: h.name, hostStale: h.stale, retentionHours: h.retentionHours })));
|
|
37522
|
+
card2 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
|
|
37523
|
+
stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
|
|
37524
|
+
rowId = (r) => r.id ?? r.jobId;
|
|
37525
|
+
canOpen = (r) => !String(rowId(r)).startsWith("sha256:") && r.canOpen !== false && ["local", "podman"].includes(r.v ?? r.backend);
|
|
37526
|
+
selected2 = (v) => report(v)?.selected ?? {};
|
|
37527
|
+
facts4 = (plan) => facts([
|
|
37528
|
+
["Image", plan.image ?? "not published"],
|
|
37529
|
+
["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
|
|
37530
|
+
["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
|
|
37531
|
+
["Credentials", "None in the shell"]
|
|
37532
|
+
]);
|
|
37533
|
+
links = (v, r, verb = "open", compact2 = false) => sandboxLinks({ id: rowId(r), host: r.host, verb, compact: compact2, config: v.data?.sandboxHosts });
|
|
37534
|
+
retention = (r, now) => html`${r.pin ? "Pinned" : "Kept"}${Number.isFinite(r.until) ? html` until ${stamp(r.until)}${r.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r.until - now)} left`}` : ": deadline not published"}`;
|
|
37535
|
+
pinForm = (v, r) => acts2(v) && canOpen(r) ? actForm({ action: "sandbox-pin", hidden: [["host", r.host], ["id", rowId(r)]], label: "Preview pin", icon: "clock" }) : null;
|
|
37536
|
+
SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
|
|
37537
|
+
}
|
|
37538
|
+
});
|
|
37539
|
+
|
|
37540
|
+
// src/zero-touch-view.mjs
|
|
37541
|
+
function breakdown(row2, repository = false) {
|
|
37542
|
+
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: number2(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number2(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number2(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) };
|
|
37543
|
+
if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r) => breakdown(r, true)) : [];
|
|
37544
|
+
return out;
|
|
37545
|
+
}
|
|
37546
|
+
function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
|
|
37547
|
+
const observed = raw?.coverage ?? raw?.window;
|
|
37548
|
+
const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number2(observed.fromMs) !== null && observed.fromMs >= (number2(raw?.nowMs) ?? nowMs2);
|
|
37549
|
+
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: [] } };
|
|
37550
|
+
const totals = raw.totals ?? raw.summary ?? {};
|
|
37551
|
+
const coverage2 = raw.coverage ?? raw.window ?? {};
|
|
37552
|
+
const out = {
|
|
37553
|
+
state: "ready",
|
|
37554
|
+
nowMs: number2(raw.nowMs) ?? nowMs2,
|
|
37555
|
+
windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
|
|
37556
|
+
coverage: { requestedFromMs: number2(coverage2.requestedFromMs), fromMs: number2(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
|
|
37557
|
+
totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number2(totals.rate), previousRate: number2(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number2(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number2(totals.medianWaitMs) ?? (number2(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 },
|
|
37558
|
+
projects: Array.isArray(raw.projects) ? raw.projects.map((r) => breakdown(r)) : [],
|
|
37559
|
+
causes: causes(raw.causes),
|
|
37560
|
+
trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r) => ({ atMs: number2(r?.atMs ?? r?.at), rate: number2(r?.rate), runs: count5(r?.runs), touched: count5(r?.touched), partial: r?.partial === true })) : [],
|
|
37561
|
+
waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r) => ({ runId: id5(r?.runId ?? r?.jobId), parkedAt: number2(r?.parkedAt) })).filter((r) => r.runId && r.parkedAt !== null) : [],
|
|
37562
|
+
touchedRunIds: runIds(raw.touchedRunIds),
|
|
37563
|
+
ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
|
|
37564
|
+
groups: [],
|
|
37565
|
+
fixes: [],
|
|
37566
|
+
fixesPartial: raw.fixesPartial === true,
|
|
37567
|
+
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((r) => ({ runId: id5(r?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r?.status) ? r.status : "source-unavailable" })).filter((r) => r.runId) : [] }
|
|
37568
|
+
};
|
|
37569
|
+
out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f) => ({ id: id5(f?.id), at: number2(f?.at), undoneAt: number2(f?.undoneAt), kind: ["instructions", "config"].includes(f?.kind) ? f.kind : "config", groupId: id5(f?.groupId), reason: typeof f?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f.reason) ? f.reason : null, triggerIndex: Number.isSafeInteger(f?.triggerIndex) && f.triggerIndex >= 0 ? f.triggerIndex : null, beforeCount: number2(f?.beforeCount), afterCount: number2(f?.afterCount), spanMs: number2(f?.spanMs), partial: f?.partial !== false, subsequentRuns: count5(f?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f?.status) ? f.status : "observing" })).filter((f) => f.id) : [];
|
|
37570
|
+
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: number2(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
|
|
37571
|
+
return out;
|
|
37572
|
+
}
|
|
37573
|
+
var CAUSES, id5, number2, count5, text3, scope, causes, runIds;
|
|
37574
|
+
var init_zero_touch_view = __esm({
|
|
37575
|
+
"src/zero-touch-view.mjs"() {
|
|
37576
|
+
CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
|
|
37577
|
+
id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
|
|
37578
|
+
number2 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
|
|
37579
|
+
count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
|
|
37580
|
+
text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
|
|
37581
|
+
scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
|
|
37582
|
+
causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
|
|
37583
|
+
runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
|
|
37584
|
+
}
|
|
37585
|
+
});
|
|
37586
|
+
|
|
37587
|
+
// src/console/panels/zero-touch.mjs
|
|
37588
|
+
function zeroTouchTabs(view) {
|
|
37589
|
+
const here = view?.page === "zero-touch";
|
|
37590
|
+
const sandboxes = view?.page === "sandboxes";
|
|
37591
|
+
return tabs([{ label: "All runs", href: "/runs", icon: "runs", current: !here && !sandboxes }, { label: "Zero touch", href: "/runs/zero-touch", icon: "flag", current: here }, ...view?.role === "operator" ? [{ label: "Sandboxes", href: "/runs/sandboxes", icon: "box", current: sandboxes }] : []], { label: "Runs views" });
|
|
37592
|
+
}
|
|
37593
|
+
function zeroTouchControls(view) {
|
|
37594
|
+
const m = model3(view);
|
|
37595
|
+
return seg([7, 30, 90].map((days) => ({ label: `${days} days`, href: `/runs/zero-touch?${new URLSearchParams({ days: String(days), ...view?.query?.project ? { project: view.query.project } : {}, ...view?.query?.scope ? { scope: view.query.scope } : {} })}`, on: days === m.windowDays })), { label: "Window" });
|
|
37596
|
+
}
|
|
37597
|
+
function delta(n2, previousRate = null) {
|
|
37598
|
+
if (!Number.isFinite(n2)) return html`<span class="zt-tr">Previous interval not covered</span>`;
|
|
37599
|
+
return html`<span class="${`zt-tr${n2 < 0 ? " tone-ok" : n2 > 0 ? " tone-attention" : ""}`}">${icon(n2 < 0 ? "chev" : n2 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n2 === 0 ? "no change" : `${n2 < 0 ? "down" : "up"} ${amount2(Math.abs(n2))} points`}</b>${previousRate === null ? null : html`<small>from ${pct(previousRate)}</small>`}</span>`;
|
|
37600
|
+
}
|
|
37601
|
+
function coverage(m) {
|
|
37602
|
+
return html`<p class="zt-coverage" role="status">${m.coverage.partial ? html`<b>Partial ${m.windowDays}-day window.</b> Covered from ${date(m.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m.windowDays}-day window.`}${m.coverage.reasons.length ? html` ${m.coverage.reasons.join("; ")}.` : null}</p>`;
|
|
37603
|
+
}
|
|
37604
|
+
function spark(m) {
|
|
37605
|
+
const rows3 = m.trend;
|
|
37606
|
+
if (!rows3.some((r) => Number.isFinite(r.rate) && !r.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
|
|
37607
|
+
const top = Math.max(32, ...rows3.map((r) => r.rate ?? 0));
|
|
37608
|
+
const x = (i) => rows3.length === 1 ? 200 : i * 200 / (rows3.length - 1);
|
|
37609
|
+
const y = (n2) => 56 - n2 / top * 56;
|
|
37610
|
+
const segments = [];
|
|
37611
|
+
let points = [];
|
|
37612
|
+
for (const [i, row2] of rows3.entries()) {
|
|
37613
|
+
if (row2.rate === null || row2.partial) {
|
|
37614
|
+
if (points.length) segments.push(points);
|
|
37615
|
+
points = [];
|
|
37616
|
+
} else points.push(`${svgNum(x(i), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
|
|
37617
|
+
}
|
|
37618
|
+
if (points.length) segments.push(points);
|
|
37619
|
+
const last = rows3[rows3.length - 1];
|
|
37620
|
+
const label2 = `Rolling ${m.windowDays}-day touch rate at weekly intervals: ${rows3.map((r) => r.partial || r.rate === null ? "not covered" : pct(r.rate)).join(", ")}. Goal under 5 percent.`;
|
|
37621
|
+
return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label2}"><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">${label2}</p>`;
|
|
37622
|
+
}
|
|
37623
|
+
function longest(m) {
|
|
37624
|
+
return [...m.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
|
|
37625
|
+
}
|
|
37626
|
+
function waitingHistory(m) {
|
|
37627
|
+
return m.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;
|
|
37628
|
+
}
|
|
37629
|
+
function compact(m) {
|
|
37630
|
+
const best = longest(m);
|
|
37631
|
+
return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount2(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m.totals.waitingPartial ? "Not known" : `${amount2(m.totals.waitingHours)}h, median ${span(m.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m.totals.draftAnswers} ${m.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m.totals.waitingPartial ? "Not known" : `${m.totals.pending} ${m.totals.pending === 1 ? "run" : "runs"}`}</b>${m.waitingRuns.slice(0, 3).map((r) => html`<span>${link(r.runId)}, ${span(m.nowMs - r.parkedAt)}</span>`)}</li></ul>${waitingHistory(m)}`;
|
|
37632
|
+
}
|
|
37633
|
+
function zeroTouchRate(view) {
|
|
37634
|
+
const m = model3(view);
|
|
37635
|
+
return card({ title: `Touch rate, ${m.windowDays} days`, body: html`${fail4(m)}${m.state === "ready" ? html`${kpi(m.totals.runs ? pct(m.totals.rate) : "No runs", `${m.totals.touched} of ${m.totals.runs} runs needed a person`)}${spark(m)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m.totals.deltaPoints, m.totals.previousRate)}</p>${coverage(m)}<p class="zt-kps">${m.totals.stopped} stopped before they ran, outside the rate.${m.totals.stoppedUnattributed > 0 ? html` ${m.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m)}</div>` : null}` });
|
|
37636
|
+
}
|
|
37637
|
+
function zeroTouchStreak(view) {
|
|
37638
|
+
const m = model3(view), best = longest(m);
|
|
37639
|
+
return card({ title: "Zero-touch streak", body: html`${fail4(m)}${kpi(best ? `${amount2(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">${m.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" : `${amount2(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
|
|
37640
|
+
}
|
|
37641
|
+
function zeroTouchWait(view) {
|
|
37642
|
+
const m = model3(view);
|
|
37643
|
+
if (m.state !== "ready") return card({ title: "Waiting for a person", body: fail4(m) });
|
|
37644
|
+
return card({ title: "Waiting for a person", body: html`${fail4(m)}${kpi(m.totals.waitingPartial ? "Not known" : `${amount2(m.totals.waitingHours)}h`, `runs waited on a person in ${m.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m.totals.medianWaitMs)}</b></p>${waitingHistory(m)}<p class="zt-kp">${chip(m.totals.waitingPartial ? "Pending count not known" : `${m.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m.waitingRuns.slice(0, 3).map((r) => html`<span>${link(r.runId)}, ${span(m.nowMs - r.parkedAt)}</span>`)}</p>` });
|
|
37645
|
+
}
|
|
37646
|
+
function zeroTouchHandled(view) {
|
|
37647
|
+
const m = model3(view);
|
|
37648
|
+
return card({ title: "Answered from a draft", body: m.state !== "ready" ? fail4(m) : html`${kpi(m.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
|
|
37649
|
+
}
|
|
37650
|
+
function meter2(row2) {
|
|
37651
|
+
if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
|
|
37652
|
+
const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
37653
|
+
const word4 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
37654
|
+
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}: ${pct(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>${pct(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word4}</span></span></span>`;
|
|
37655
|
+
}
|
|
37656
|
+
function rowContent(row2, sub2 = false) {
|
|
37657
|
+
const c = causesByKey.get(row2.topCause);
|
|
37658
|
+
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((r) => r.name).join(" and ")}</small></span>${meter2(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" : `${amount2(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>`;
|
|
37659
|
+
}
|
|
37660
|
+
function phoneProjects(m, view) {
|
|
37661
|
+
const item2 = (row2, repository = false) => {
|
|
37662
|
+
const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
37663
|
+
const word4 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
37664
|
+
const query = new URLSearchParams({ days: String(m.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
|
|
37665
|
+
return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct(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>${pct(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word4}</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>`;
|
|
37666
|
+
};
|
|
37667
|
+
return html`<ul class="ztp-ps zt-phone-projects">${m.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r) => item2(r, true)) : null}`)}</ul>`;
|
|
37668
|
+
}
|
|
37669
|
+
function zeroTouchProjects(view) {
|
|
37670
|
+
const m = model3(view);
|
|
37671
|
+
if (m.state !== "ready") return card({ title: "By project", body: fail4(m) });
|
|
37672
|
+
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>`;
|
|
37673
|
+
const rows3 = m.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}: ${pct(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r) => html`<li class="zt-row zt-row--sub">${rowContent(r, 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(m.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
|
|
37674
|
+
return card({ title: "By project", sub: `${m.totals.touched} of ${m.totals.runs} runs needed a person`, flush: true, body: html`${fail4(m)}${m.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m, 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>`}` });
|
|
37675
|
+
}
|
|
37676
|
+
function causeCounts(values) {
|
|
37677
|
+
return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
|
|
37678
|
+
}
|
|
37679
|
+
function causeBar(values, max, name3) {
|
|
37680
|
+
const rows3 = causeCounts(values);
|
|
37681
|
+
let at2 = 0;
|
|
37682
|
+
max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
|
|
37683
|
+
const all = rows3.filter((r) => r.count > 0);
|
|
37684
|
+
const label2 = `${name3}: ${all.map((r) => `${r.label} ${r.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
|
|
37685
|
+
return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label2}">${all.map((r) => {
|
|
37686
|
+
const x = at2 / Math.max(1, max) * 500, w = r.count / Math.max(1, max) * 500;
|
|
37687
|
+
at2 += r.count;
|
|
37688
|
+
return html`<g class="${`tone-${r.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x / 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((x + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r.count}</text>` : null}</g>`;
|
|
37689
|
+
})}</svg>`;
|
|
37690
|
+
}
|
|
37691
|
+
function zeroTouchCauses(view) {
|
|
37692
|
+
const m = model3(view), max = Math.max(1, ...m.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
|
|
37693
|
+
if (m.state !== "ready") return card({ title: "What needed a person", body: fail4(m) });
|
|
37694
|
+
const bars = m.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>`);
|
|
37695
|
+
const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
|
|
37696
|
+
return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m.windowDays} days, by project</span><span class="zt-phone-label">${m.windowDays} days, ${scopeLabel}</span>`, body: html`${fail4(m)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m.causes, Math.max(1, m.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m.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">${m.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>` });
|
|
37697
|
+
}
|
|
37698
|
+
function guardedForm(action2, { hidden: hidden2 = [], fields: fields2 = null, label: label2, kind = ["sm"] } = {}) {
|
|
37699
|
+
return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name3, value, type]) => html`<input type="hidden" name="${name3}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields2}${btn({ type: "submit", kind, label: label2 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
|
|
37700
|
+
}
|
|
37701
|
+
function nameForm(g, index2) {
|
|
37702
|
+
const key2 = `zt-name-${index2}`;
|
|
37703
|
+
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: input2({ 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>`;
|
|
37704
|
+
}
|
|
37705
|
+
function instructionForm(g, index2) {
|
|
37706
|
+
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>`;
|
|
37707
|
+
const key2 = `zt-sentence-${index2}`;
|
|
37708
|
+
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>`;
|
|
37709
|
+
}
|
|
37710
|
+
function zeroTouchQuestions(view) {
|
|
37711
|
+
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." });
|
|
37712
|
+
const m = model3(view);
|
|
37713
|
+
if (m.state !== "ready") return card({ title: "Questions it keeps asking", body: fail4(m) });
|
|
37714
|
+
const rows3 = m.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" && m.fixes.some((f) => f.groupId === g.id && f.afterCount === 0 && f.subsequentRuns > 0 && !f.partial && f.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((id6, at2) => html`${at2 ? ", " : ""}${link(id6)}`)}${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>`);
|
|
37715
|
+
const phoneRows = m.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>`);
|
|
37716
|
+
const sources = m.grouping.sources.length ? m.grouping.sources : m.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
|
|
37717
|
+
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>${fail4(m)}${m.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(m.grouping.state) ? html`<p class="zt-note" role="status">${m.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(m.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>${m.grouping.enabled ? `${plural5(m.grouping.pending, "grouping job")} pending; ${plural5(m.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>` });
|
|
37718
|
+
}
|
|
37719
|
+
function fixTone(f) {
|
|
37720
|
+
if (f.partial || f.subsequentRuns === 0 || f.undoneAt !== null || f.status === "observing") return "idle";
|
|
37721
|
+
return f.status === "improved" ? "ok" : f.status === "worse" ? "failed" : "idle";
|
|
37722
|
+
}
|
|
37723
|
+
function fixObservation(f) {
|
|
37724
|
+
if (f.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
|
|
37725
|
+
if (f.partial) return "Partial observation: effectiveness is not established.";
|
|
37726
|
+
if (f.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
|
|
37727
|
+
return f.status === "improved" ? "Fewer occurrences over equal observed spans." : f.status === "worse" ? "More occurrences over equal observed spans." : f.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
|
|
37728
|
+
}
|
|
37729
|
+
function beforeAfter(label2, value, max, tone = "attention") {
|
|
37730
|
+
return html`<p class="zt-ba"><span>${label2}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label2}: ${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>`;
|
|
37731
|
+
}
|
|
37732
|
+
function zeroTouchFixes(view) {
|
|
37733
|
+
const m = model3(view);
|
|
37734
|
+
const evidence = html`${m.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}${m.fixes.length ? html`<ol class="zt-fxs">${m.fixes.map((f) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f.at)} UTC${f.undoneAt === null ? null : html` · Reverted ${date(f.undoneAt)} UTC`}</p><p class="zt-fx__w">${f.kind === "instructions" ? `Trigger ${f.triggerIndex === null ? "not known" : f.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f.reason ? `${f.reason} refusals` : "Questions in the targeted group"}, equal ${span(f.spanMs)} spans before and after; ${f.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f.beforeCount, Math.max(f.beforeCount ?? 0, f.afterCount ?? 0, 1))}${beforeAfter("After", f.afterCount, Math.max(f.beforeCount ?? 0, f.afterCount ?? 0, 1), fixTone(f))}<p class="zt-kps">${fixObservation(f)}</p>${acts3(view) && f.kind === "instructions" && f.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
|
|
37735
|
+
const brief = m.fixes.map((f) => `${f.reason ?? "grouped questions"}: ${f.beforeCount ?? "?"} to ${f.afterCount ?? "?"}`).join("; ");
|
|
37736
|
+
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>${m.fixesPartial ? "Fix history incomplete" : `${m.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
|
|
37737
|
+
}
|
|
37738
|
+
var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model3, acts3, pct, amount2, date, span, link, fail4, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
|
|
37739
|
+
var init_zero_touch = __esm({
|
|
37740
|
+
"src/console/panels/zero-touch.mjs"() {
|
|
37741
|
+
init_common();
|
|
37742
|
+
init_html();
|
|
37743
|
+
init_kit();
|
|
37744
|
+
init_zero_touch_view();
|
|
37745
|
+
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." });
|
|
37746
|
+
ZERO_TOUCH_CAUSES = Object.freeze([
|
|
37747
|
+
{ cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
|
|
37748
|
+
{ cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
|
|
37749
|
+
{ 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." },
|
|
37750
|
+
{ 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." },
|
|
37751
|
+
{ cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
|
|
37752
|
+
{ cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
|
|
37753
|
+
]);
|
|
37754
|
+
ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
|
|
37755
|
+
unassigned: "Not grouped yet.",
|
|
37756
|
+
pending: "A grouping job is pending.",
|
|
37757
|
+
assigned: "Assigned to a question group.",
|
|
37758
|
+
"source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
|
|
37759
|
+
"source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
|
|
37760
|
+
"source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
|
|
37761
|
+
"candidates-expired": "A group's representative context expired. Review the groups before trying again.",
|
|
37762
|
+
"group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
|
|
37763
|
+
stale: "The group revision changed during classification. Preview a fresh grouping job.",
|
|
37764
|
+
refused: "The grouping job was refused. Open its run to see the policy reason.",
|
|
37765
|
+
failed: "The grouping job failed. Open its run and review the failure before trying again."
|
|
37766
|
+
});
|
|
37767
|
+
causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
|
|
37768
|
+
model3 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
|
|
37769
|
+
acts3 = (v) => v?.role === "operator" && !v?.readOnly;
|
|
37770
|
+
pct = (n2) => Number.isFinite(n2) ? `${Math.round(n2)}%` : "Not known";
|
|
37771
|
+
amount2 = (n2) => Number.isFinite(n2) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n2) : "Not known";
|
|
37772
|
+
date = (n2) => Number.isFinite(n2) && n2 >= 0 && n2 <= 864e13 ? new Date(n2).toISOString().slice(0, 10) : "not known";
|
|
37773
|
+
span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount2(ms3 / 36e5)}h` : `${amount2(ms3 / 6e4)}m`;
|
|
37774
|
+
link = (id6) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id6)}`}">${id6}</a>`;
|
|
37775
|
+
fail4 = (m) => m.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
|
|
37776
|
+
ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
|
|
37777
|
+
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]]);
|
|
37778
|
+
}
|
|
37779
|
+
});
|
|
37780
|
+
|
|
37781
|
+
// src/trigger-policy.mjs
|
|
37782
|
+
function triggerWhoMayFire(trigger2) {
|
|
37783
|
+
const on = trigger2.on ?? trigger2;
|
|
37784
|
+
const forge = trigger2.run?.kind ?? trigger2.forge;
|
|
37785
|
+
const kind = on.type;
|
|
37786
|
+
if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
|
|
37787
|
+
if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
|
|
37788
|
+
if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
|
|
37789
|
+
if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
|
|
37790
|
+
if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
|
|
37791
|
+
if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
|
|
37792
|
+
const actions = Array.isArray(on.action) ? on.action : [];
|
|
37793
|
+
const gates = [];
|
|
37794
|
+
if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
|
|
37795
|
+
if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
|
|
37796
|
+
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");
|
|
37797
|
+
if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
|
|
37798
|
+
return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
|
|
37799
|
+
}
|
|
37800
|
+
var init_trigger_policy = __esm({
|
|
37801
|
+
"src/trigger-policy.mjs"() {
|
|
37712
37802
|
}
|
|
37713
37803
|
});
|
|
37714
37804
|
|
|
@@ -62515,11 +62605,37 @@ function settingDone(out) {
|
|
|
62515
62605
|
const sayable = typeof old === "string" && old.trim() !== "" || typeof old === "number" && Number.isFinite(old);
|
|
62516
62606
|
return sayable ? `Changed ${key2} from ${String(old)} to ${value}` : `Changed ${key2} to ${value}`;
|
|
62517
62607
|
}
|
|
62608
|
+
function pauseListShapeRefusal(windows) {
|
|
62609
|
+
for (const [i, w] of windows.entries()) {
|
|
62610
|
+
const at2 = atIndex("pause window", i);
|
|
62611
|
+
if (w === null || typeof w !== "object" || Array.isArray(w)) return `${at2} is not an object`;
|
|
62612
|
+
for (const [k, value] of Object.entries(w)) {
|
|
62613
|
+
if (!PAUSE_WINDOW_FIELDS.includes(k)) return `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only)`;
|
|
62614
|
+
const plain3 = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
|
|
62615
|
+
if (!plain3) return `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}`;
|
|
62616
|
+
}
|
|
62617
|
+
}
|
|
62618
|
+
return null;
|
|
62619
|
+
}
|
|
62518
62620
|
function pauseListBack(out) {
|
|
62519
62621
|
const unread2 = typeof out.beforeText === "string" && !Array.isArray(jsonOf(out.beforeText)?.windows);
|
|
62520
62622
|
if (unread2) return { undo: twice(UNDO_SENTENCES.tooBig, UNDO_SENTENCES.tooBigAfter), inverse: null };
|
|
62623
|
+
if (!pauseListGoesBack(out)) return { undo: twice(UNDO_SENTENCES.refusedList, UNDO_SENTENCES.refusedListAfter), inverse: null };
|
|
62521
62624
|
return { undo: "Put the previous pause windows back", inverse: { action: "pause-windows", body: { windows: out.extra.before } } };
|
|
62522
62625
|
}
|
|
62626
|
+
function pauseListGoesBack(out) {
|
|
62627
|
+
const before = out.extra?.before;
|
|
62628
|
+
if (!Array.isArray(before) || !plainBody({ windows: before }) || pauseListShapeRefusal(before) !== null) return false;
|
|
62629
|
+
if (typeof out.beforeText !== "string") return true;
|
|
62630
|
+
const held = heldPauseWindows(out.beforeText, out.target ?? "pause-windows.json");
|
|
62631
|
+
if (held.problem) return false;
|
|
62632
|
+
try {
|
|
62633
|
+
parsePauseWindows(pauseWindowsText(held.doc, before), out.target ?? "pause-windows.json");
|
|
62634
|
+
return true;
|
|
62635
|
+
} catch {
|
|
62636
|
+
return false;
|
|
62637
|
+
}
|
|
62638
|
+
}
|
|
62523
62639
|
function triggerEditBack(out) {
|
|
62524
62640
|
const p = out.plan ?? {};
|
|
62525
62641
|
const n2 = atIndex("trigger", p.index);
|
|
@@ -63001,15 +63117,8 @@ var init_actions = __esm({
|
|
|
63001
63117
|
needs: [],
|
|
63002
63118
|
async preview(body, ctx) {
|
|
63003
63119
|
if (!Array.isArray(body.windows)) return { refusal: 'send {"windows": [...]} with the whole new list of pause windows' };
|
|
63004
|
-
|
|
63005
|
-
|
|
63006
|
-
if (w === null || typeof w !== "object" || Array.isArray(w)) return { refusal: `${at2} is not an object; nothing was changed` };
|
|
63007
|
-
for (const [k, value] of Object.entries(w)) {
|
|
63008
|
-
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` };
|
|
63009
|
-
const plain3 = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
|
|
63010
|
-
if (!plain3) return { refusal: `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}; nothing was changed` };
|
|
63011
|
-
}
|
|
63012
|
-
}
|
|
63120
|
+
const shape2 = pauseListShapeRefusal(body.windows);
|
|
63121
|
+
if (shape2) return { refusal: `${shape2}; nothing was changed` };
|
|
63013
63122
|
const path = ctx.config.pauseWindowsPath;
|
|
63014
63123
|
const snap = readNow(ctx.fs, path);
|
|
63015
63124
|
if (snap.refusal) return snap;
|
|
@@ -63687,6 +63796,8 @@ var init_actions = __esm({
|
|
|
63687
63796
|
envelopeAfter: "Set it back with the envelope form: the console kept no copy of the old values.",
|
|
63688
63797
|
tooBig: "Put the previous list back by hand: this preview showed it; the console keeps no copy.",
|
|
63689
63798
|
tooBigAfter: "Put the previous list back by hand: the console kept no copy of it.",
|
|
63799
|
+
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.",
|
|
63800
|
+
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.",
|
|
63690
63801
|
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.",
|
|
63691
63802
|
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."
|
|
63692
63803
|
});
|
|
@@ -74188,6 +74299,8 @@ async function readScheduleHosts({ url, now = () => Date.now(), timeoutMs = 2e3,
|
|
|
74188
74299
|
let redis;
|
|
74189
74300
|
try {
|
|
74190
74301
|
redis = redisFn(url, { failFast: true, disconnectTimeoutMs: 0 });
|
|
74302
|
+
redis.on?.("error", () => {
|
|
74303
|
+
});
|
|
74191
74304
|
const names2 = await bounded14(() => redis.smembers(HOST_SET), timeoutMs, null);
|
|
74192
74305
|
if (!Array.isArray(names2)) return { hosts: [], complete: false };
|
|
74193
74306
|
const valid2 = [...new Set(names2.filter((n2) => typeof n2 === "string" && NAME_RE2.test(n2)))];
|
|
@@ -74539,7 +74652,10 @@ async function readConnections({ deploymentDir, env = {}, paths = {}, snapshot:
|
|
|
74539
74652
|
if (redis) return redis;
|
|
74540
74653
|
if (owned) return owned;
|
|
74541
74654
|
if (injectedRead && typeof paths.valkeyUrl !== "string") return null;
|
|
74542
|
-
|
|
74655
|
+
owned = redisFn(paths.valkeyUrl);
|
|
74656
|
+
owned.on?.("error", () => {
|
|
74657
|
+
});
|
|
74658
|
+
return owned;
|
|
74543
74659
|
};
|
|
74544
74660
|
const endpointFleetDeclared = endpointFleetDeclaration(file, environmentUnknown);
|
|
74545
74661
|
let endpointStatus = { state: "unknown", hosts: [], more: 0, partial: true };
|
|
@@ -76315,14 +76431,20 @@ function makeSandboxService({ paths, env = {}, deploymentDir, fleet, fs: fs2 = n
|
|
|
76315
76431
|
}
|
|
76316
76432
|
const selected4 = { repo: query.repo ?? sourceRun?.repo ?? null, index: query.index, ref: query.ref ?? "HEAD", host: query.host ?? (query.fromRun ? sourceRun?.host ?? null : inv.hosts.find((h) => !h.stale && !h.unreadable && h.settings)?.name ?? null), pin: query.pin === true };
|
|
76317
76433
|
if (selected4.index === null || selected4.index === void 0) selected4.index = query.fromRun ? null : options[0]?.index ?? null;
|
|
76318
|
-
const rawTrigger = cfg.triggers[selected4.index]?.
|
|
76434
|
+
const rawTrigger = cfg.triggers[selected4.index]?.on?.type === "disarmed" ? (() => {
|
|
76435
|
+
try {
|
|
76436
|
+
return JSON.parse(cfg.triggersText).triggers?.[selected4.index]?.run;
|
|
76437
|
+
} catch {
|
|
76438
|
+
return void 0;
|
|
76439
|
+
}
|
|
76440
|
+
})() : cfg.triggers[selected4.index]?.run;
|
|
76319
76441
|
const eligibleHosts = inv.hosts.filter((h) => {
|
|
76320
76442
|
const backend = rawTrigger?.backend ?? h.settings?.defaultBackend;
|
|
76321
76443
|
return !h.stale && !h.unreadable && h.settings && ["local", "podman"].includes(backend) && h.settings.backends.includes(backend);
|
|
76322
76444
|
}).map((h) => h.name);
|
|
76323
76445
|
const judged = selected4.repo && selected4.host && selected4.index !== null ? preparation(selected4, inv, cfg) : {};
|
|
76324
76446
|
const request = query.request && query.host ? await fleet.sandboxPreparationStatus(query.host, query.request) : null;
|
|
76325
|
-
return { ...inv, options, eligibleHosts, selected: selected4, withholdsPackages: rawTrigger?.packages === false, excludes: Array.isArray(rawTrigger?.excludeExtensions) ? rawTrigger.excludeExtensions : [], ...judged, request, ...query.fromRun ? { fromRun: query.fromRun, sourceRun } : {} };
|
|
76447
|
+
return { ...inv, options, eligibleHosts, selected: selected4, withholdsPackages: rawTrigger?.packages === false, flow: typeof rawTrigger?.flow === "string" ? rawTrigger.flow : null, mcp: Array.isArray(rawTrigger?.mcp) ? rawTrigger.mcp.filter((name3) => typeof name3 === "string") : [], excludes: Array.isArray(rawTrigger?.excludeExtensions) ? rawTrigger.excludeExtensions : [], ...judged, request, ...query.fromRun ? { fromRun: query.fromRun, sourceRun } : {} };
|
|
76326
76448
|
},
|
|
76327
76449
|
async preview(name3, params) {
|
|
76328
76450
|
if (name3 === "sandbox-host-set") {
|