@edgehero/pi-dispatch-admin 5.1.0 → 5.2.1
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 +32760 -32177
- package/dist/index.mjs +822 -464
- package/dist/sandbox-session-monitor.mjs +119 -8
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -7620,6 +7620,13 @@ var init_worker_name = __esm({
|
|
|
7620
7620
|
});
|
|
7621
7621
|
|
|
7622
7622
|
// ../worker/src/job-size.mjs
|
|
7623
|
+
function wholeCpusOf(cpuCenti) {
|
|
7624
|
+
return Math.ceil(cpuCenti / 100);
|
|
7625
|
+
}
|
|
7626
|
+
function jobSizeEnv(size) {
|
|
7627
|
+
assertJobSize(size);
|
|
7628
|
+
return { PI_JOB_SIZE_CPUS: String(wholeCpusOf(size.cpuCenti)), PI_JOB_SIZE_MEMORY_MIB: String(size.memMiB) };
|
|
7629
|
+
}
|
|
7623
7630
|
function parseMemory(value) {
|
|
7624
7631
|
if (typeof value !== "string" || !/^[1-9]\d*[mg]$/.test(value)) {
|
|
7625
7632
|
throw new Error(`a memory size is a whole number of megabytes or gigabytes such as "512m", "1536m" or "4g" (got ${JSON.stringify(value)})`);
|
|
@@ -8803,6 +8810,19 @@ function roundCpusUp(cpuCenti) {
|
|
|
8803
8810
|
function rank(sorted, pct5) {
|
|
8804
8811
|
return sorted[Math.max(0, Math.ceil(pct5 * sorted.length / 100) - 1)];
|
|
8805
8812
|
}
|
|
8813
|
+
function seenOf(record3) {
|
|
8814
|
+
if (!CEILING_VENUES.includes(record3.backend)) return null;
|
|
8815
|
+
const c = record3.capacity;
|
|
8816
|
+
if (c === null || typeof c !== "object" || Array.isArray(c)) return null;
|
|
8817
|
+
const cpus = Number.isSafeInteger(c.cpus) && c.cpus >= 1 && c.cpus <= HOST_CPUS_MAX ? c.cpus : null;
|
|
8818
|
+
const budget = Number.isSafeInteger(c.cpuCenti) && c.cpuCenti >= 1 && c.cpuCenti <= HOST_CPUS_MAX * 100 ? c.cpuCenti : null;
|
|
8819
|
+
const ceilingCenti = cpuCeilingCenti(cpus, budget);
|
|
8820
|
+
return ceilingCenti === null ? null : seenCpusOf(ceilingCenti, cpus);
|
|
8821
|
+
}
|
|
8822
|
+
function seenCpusOf(ceilingCenti, runtimeCpus) {
|
|
8823
|
+
if (ceilingCenti >= 100) return Math.floor(ceilingCenti / 100);
|
|
8824
|
+
return Number.isSafeInteger(runtimeCpus) && runtimeCpus >= 1 ? runtimeCpus : null;
|
|
8825
|
+
}
|
|
8806
8826
|
function evidenceOf(record3, project2, nowMs2) {
|
|
8807
8827
|
if (record3 === null || typeof record3 !== "object" || Array.isArray(record3)) return null;
|
|
8808
8828
|
if (record3.project !== project2) return null;
|
|
@@ -8822,7 +8842,7 @@ function evidenceOf(record3, project2, nowMs2) {
|
|
|
8822
8842
|
};
|
|
8823
8843
|
const memFullUsec = wallUsec !== null && isCount(r2.memFullUsec) ? r2.memFullUsec : null;
|
|
8824
8844
|
if (memPeak === null && cpu.cpuUsec === null) return null;
|
|
8825
|
-
return { at: at2, size, memPeak, oom: record3.reason === "oom-killed", wallUsec, ...cpu, memFullUsec };
|
|
8845
|
+
return { at: at2, size, memPeak, oom: record3.reason === "oom-killed", wallUsec, ...cpu, memFullUsec, seenCpus: seenOf(record3) };
|
|
8826
8846
|
}
|
|
8827
8847
|
function heldToCap(wanted, current, cap) {
|
|
8828
8848
|
if (wanted <= current) return { suggested: null, held: "largest" };
|
|
@@ -8831,6 +8851,14 @@ function heldToCap(wanted, current, cap) {
|
|
|
8831
8851
|
if (wanted > cap) return { suggested: cap, held: "cap" };
|
|
8832
8852
|
return { suggested: wanted, held: null };
|
|
8833
8853
|
}
|
|
8854
|
+
function oomCeilingOf(relevant) {
|
|
8855
|
+
const over = relevant.filter((e2) => e2.oom && e2.seenCpus !== null && e2.seenCpus > wholeCpusOf(e2.size.cpuCenti));
|
|
8856
|
+
if (over.length === 0) return null;
|
|
8857
|
+
const seen = over.map((e2) => e2.seenCpus);
|
|
8858
|
+
const sizes = over.map((e2) => e2.size.cpuCenti);
|
|
8859
|
+
const sizeCenti = Math.min(...sizes);
|
|
8860
|
+
return { runs: over.length, seenCpus: Math.min(...seen), seenMaxCpus: Math.max(...seen), sizeCenti, sizeVaries: sizes.some((n3) => n3 !== sizeCenti) };
|
|
8861
|
+
}
|
|
8834
8862
|
function suggestMemory(runs, current, cap) {
|
|
8835
8863
|
const measured = runs.filter((e2) => e2.memPeak !== null);
|
|
8836
8864
|
const relevant = measured.filter((e2) => e2.size.memMiB >= current);
|
|
@@ -8841,7 +8869,7 @@ function suggestMemory(runs, current, cap) {
|
|
|
8841
8869
|
const oomSizes = measured.filter((e2) => e2.oom).map((e2) => e2.size.memMiB);
|
|
8842
8870
|
const largestOom = oomSizes.length > 0 ? Math.max(...oomSizes) : null;
|
|
8843
8871
|
const pressured = relevant.filter((e2) => !productAbove(e2.size.memMiB * MIB, 9, e2.memPeak, 10) && e2.memFullUsec !== null && productAbove(e2.memFullUsec, 100, e2.wallUsec, 1)).length;
|
|
8844
|
-
const evidence = { samples, smaller: measured.length - samples, p95MiB: p95 === null ? null : Math.ceil(p95 / MIB), maxPeakMiB: maxPeak === null ? null : Math.ceil(maxPeak / MIB), ooms, largestOomMiB: largestOom, pressured };
|
|
8872
|
+
const evidence = { samples, smaller: measured.length - samples, p95MiB: p95 === null ? null : Math.ceil(p95 / MIB), maxPeakMiB: maxPeak === null ? null : Math.ceil(maxPeak / MIB), ooms, largestOomMiB: largestOom, pressured, oomCeiling: oomCeilingOf(relevant) };
|
|
8845
8873
|
const fact = pressured > 0 ? "pressure" : null;
|
|
8846
8874
|
const out = (reason, suggested, more = {}) => ({ current, suggested: suggested === null || suggested === current ? null : suggested, reason, evidence, fact, held: null, wanted: null, ...more });
|
|
8847
8875
|
if (ooms > 0) {
|
|
@@ -8964,6 +8992,15 @@ function notEnoughRunsText(dim, sizeText3) {
|
|
|
8964
8992
|
const smaller = Number.isSafeInteger(e2.smaller) && e2.smaller > 0 ? `; ${e2.smaller} at smaller sizes` : "";
|
|
8965
8993
|
return `not enough runs at ${sizeText3(dim.current)} (${samples}${smaller})`;
|
|
8966
8994
|
}
|
|
8995
|
+
function oomCeilingWords(me) {
|
|
8996
|
+
const o = me?.oomCeiling;
|
|
8997
|
+
if (o === null || typeof o !== "object" || !Number.isSafeInteger(o.runs) || o.runs < 1 || !Number.isSafeInteger(o.seenCpus)) return "";
|
|
8998
|
+
const ooms = Number.isSafeInteger(me.ooms) && me.ooms >= o.runs ? me.ooms : o.runs;
|
|
8999
|
+
const whose = ooms === 1 ? "the killed run's tools" : o.runs === ooms ? "the killed runs' tools" : `the tools of ${o.runs} of the ${ooms} killed runs`;
|
|
9000
|
+
const seen = `${o.seenCpus}${Number.isSafeInteger(o.seenMaxCpus) && o.seenMaxCpus > o.seenCpus ? ` to ${o.seenMaxCpus}` : ""} CPUs`;
|
|
9001
|
+
const size = o.sizeVaries === true ? `sizes from ${cpusText(o.sizeCenti)}` : `a size of ${cpusText(o.sizeCenti)}`;
|
|
9002
|
+
return `size per-CPU worker pools from PI_JOB_SIZE_CPUS: ${whose} counted at least ${seen} at ${size}`;
|
|
9003
|
+
}
|
|
8967
9004
|
function suggestionEvidence(suggestion) {
|
|
8968
9005
|
const m2 = suggestion?.memory ?? {};
|
|
8969
9006
|
const c = suggestion?.cpu ?? {};
|
|
@@ -8994,10 +9031,11 @@ function suggestionEvidence(suggestion) {
|
|
|
8994
9031
|
cpu: cpuWords[c.reason] ?? "",
|
|
8995
9032
|
memoryHeld: heldWords[m2.held] ?? "",
|
|
8996
9033
|
memoryFact: m2.fact === "pressure" ? `${me.pressured} of ${plural(me.samples, "run")} reached the memory limit while stalled for memory more than 1% of their time` : "",
|
|
8997
|
-
cpuFact: c.fact === "ceiling" ? `the median run was held back ${ce.throttledPct}% of its time by this host's CPU ceiling (its CPU budget, shared by every job), which a job's cpus do not change` : ""
|
|
9034
|
+
cpuFact: c.fact === "ceiling" ? `the median run was held back ${ce.throttledPct}% of its time by this host's CPU ceiling (its CPU budget, shared by every job), which a job's cpus do not change` : "",
|
|
9035
|
+
memoryCeiling: m2.reason === "oom-killed" ? oomCeilingWords(me) : ""
|
|
8998
9036
|
};
|
|
8999
9037
|
}
|
|
9000
|
-
var SUGGEST_WINDOW_DAYS, SUGGEST_WINDOW_RUNS, SUGGEST_MIN_SAMPLES, SUGGEST_CLOCK_SKEW_MS, SUGGEST_MAX_WALL_MS, MEMORY_REASONS, CPU_REASONS, MEMORY_HELD, MEMORY_CAP_MISSING, SIZE_FACTS, MIB, DAY_MS, isCount, productAbove, quarterMoreMiB, quarterMoreCenti, capDim, isShare, shareOf, plural, coresText, cpusText;
|
|
9038
|
+
var SUGGEST_WINDOW_DAYS, SUGGEST_WINDOW_RUNS, SUGGEST_MIN_SAMPLES, SUGGEST_CLOCK_SKEW_MS, SUGGEST_MAX_WALL_MS, MEMORY_REASONS, CPU_REASONS, MEMORY_HELD, MEMORY_CAP_MISSING, SIZE_FACTS, CEILING_VENUES, MIB, DAY_MS, isCount, productAbove, quarterMoreMiB, quarterMoreCenti, capDim, isShare, shareOf, plural, coresText, cpusText;
|
|
9001
9039
|
var init_size_suggest = __esm({
|
|
9002
9040
|
"../worker/src/size-suggest.mjs"() {
|
|
9003
9041
|
init_job_size();
|
|
@@ -9011,6 +9049,7 @@ var init_size_suggest = __esm({
|
|
|
9011
9049
|
MEMORY_HELD = Object.freeze(["cap", "largest", "no-cap"]);
|
|
9012
9050
|
MEMORY_CAP_MISSING = Object.freeze(["unread", "none-holds", "off"]);
|
|
9013
9051
|
SIZE_FACTS = Object.freeze(["pressure", "ceiling"]);
|
|
9052
|
+
CEILING_VENUES = Object.freeze(["local", "podman"]);
|
|
9014
9053
|
MIB = 1024 * 1024;
|
|
9015
9054
|
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
9016
9055
|
isCount = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
@@ -10849,6 +10888,10 @@ var init_reserved_env = __esm({
|
|
|
10849
10888
|
"PI_RUNNER_OBSERVABLE",
|
|
10850
10889
|
// Issue #693: derived from the resolved job memory, never chosen by a trigger or forwarded host value.
|
|
10851
10890
|
"PI_CODEMODE_MAX_WORKERS",
|
|
10891
|
+
// Issue #865: the job's size as the job reads it (job-size.mjs `jobSizeEnv`). A trigger binding one through
|
|
10892
|
+
// run.secrets, or a forwarded host value, would tell the job's tools a size the container was not given.
|
|
10893
|
+
"PI_JOB_SIZE_CPUS",
|
|
10894
|
+
"PI_JOB_SIZE_MEMORY_MIB",
|
|
10852
10895
|
"PI_PROVIDER",
|
|
10853
10896
|
"PI_MODEL",
|
|
10854
10897
|
"PI_FORK_ENTRY",
|
|
@@ -13192,39 +13235,25 @@ function findDuplicateKey(text4) {
|
|
|
13192
13235
|
};
|
|
13193
13236
|
const skipString = () => {
|
|
13194
13237
|
i2++;
|
|
13238
|
+
let escaped = false;
|
|
13195
13239
|
while (i2 < n3) {
|
|
13196
|
-
const
|
|
13197
|
-
if (
|
|
13198
|
-
i2++;
|
|
13199
|
-
return;
|
|
13200
|
-
}
|
|
13201
|
-
i2 += ch === "\\" ? 2 : 1;
|
|
13202
|
-
}
|
|
13203
|
-
};
|
|
13204
|
-
const readString = () => {
|
|
13205
|
-
i2++;
|
|
13206
|
-
let out = "";
|
|
13207
|
-
while (i2 < n3) {
|
|
13208
|
-
const ch = s2[i2];
|
|
13209
|
-
if (ch === '"') {
|
|
13240
|
+
const c = s2.charCodeAt(i2);
|
|
13241
|
+
if (c === 34) {
|
|
13210
13242
|
i2++;
|
|
13211
|
-
return
|
|
13243
|
+
return escaped;
|
|
13212
13244
|
}
|
|
13213
|
-
if (
|
|
13214
|
-
|
|
13245
|
+
if (c === 92) {
|
|
13246
|
+
escaped = true;
|
|
13215
13247
|
i2 += 2;
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
i2 += 4;
|
|
13219
|
-
} else {
|
|
13220
|
-
out += SIMPLE_ESCAPES[esc] ?? esc;
|
|
13221
|
-
}
|
|
13222
|
-
continue;
|
|
13248
|
+
} else {
|
|
13249
|
+
i2++;
|
|
13223
13250
|
}
|
|
13224
|
-
out += ch;
|
|
13225
|
-
i2++;
|
|
13226
13251
|
}
|
|
13227
|
-
return
|
|
13252
|
+
return escaped;
|
|
13253
|
+
};
|
|
13254
|
+
const readKey = () => {
|
|
13255
|
+
const start = i2;
|
|
13256
|
+
return skipString() ? JSON.parse(s2.slice(start, i2)) : s2.slice(start + 1, i2 - 1);
|
|
13228
13257
|
};
|
|
13229
13258
|
let expectKey = false;
|
|
13230
13259
|
while (i2 < n3) {
|
|
@@ -13273,8 +13302,11 @@ function findDuplicateKey(text4) {
|
|
|
13273
13302
|
if (ch === '"') {
|
|
13274
13303
|
const top = stack[stack.length - 1];
|
|
13275
13304
|
if (expectKey && top && top.keys !== null) {
|
|
13276
|
-
const key2 =
|
|
13277
|
-
if (top.keys.has(key2))
|
|
13305
|
+
const key2 = readKey();
|
|
13306
|
+
if (top.keys.has(key2)) {
|
|
13307
|
+
const own2 = JSON.parse(JSON.stringify(key2));
|
|
13308
|
+
return { key: own2, at: [...path6, own2].join(".") };
|
|
13309
|
+
}
|
|
13278
13310
|
top.keys.add(key2);
|
|
13279
13311
|
path6.push(key2);
|
|
13280
13312
|
top.named = true;
|
|
@@ -13288,10 +13320,9 @@ function findDuplicateKey(text4) {
|
|
|
13288
13320
|
}
|
|
13289
13321
|
return null;
|
|
13290
13322
|
}
|
|
13291
|
-
var
|
|
13323
|
+
var STRUCTURAL;
|
|
13292
13324
|
var init_json_duplicates = __esm({
|
|
13293
13325
|
"../worker/src/json-duplicates.mjs"() {
|
|
13294
|
-
SIMPLE_ESCAPES = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
|
|
13295
13326
|
STRUCTURAL = /* @__PURE__ */ new Set([",", ":", "{", "}", "[", "]", '"', " ", " ", "\n", "\r"]);
|
|
13296
13327
|
}
|
|
13297
13328
|
});
|
|
@@ -15037,8 +15068,9 @@ function pauseWindowsVersionRefusal(doc, { supported = PAUSE_WINDOWS_VERSION } =
|
|
|
15037
15068
|
function heldPauseWindows(text4, path6) {
|
|
15038
15069
|
if ((text4 ?? "").trim() === "") return { doc: null, windows: [] };
|
|
15039
15070
|
const { value, problem } = heldDocument(text4, "windows");
|
|
15040
|
-
const
|
|
15041
|
-
|
|
15071
|
+
const doubled = value !== void 0 && findDuplicateKey(text4) !== null;
|
|
15072
|
+
const refused3 = doubled ? "has a key written twice" : problem ?? null;
|
|
15073
|
+
if (refused3) return { doc: doubled ? null : value ?? null, problem: `the pause-windows file ${path6} ${refused3}, so its windows cannot be kept` };
|
|
15042
15074
|
return { doc: value, windows: Array.isArray(value.windows) ? value.windows : [] };
|
|
15043
15075
|
}
|
|
15044
15076
|
function pauseWindowsText(doc, windows, { table: table3 = PAUSE_WINDOW_FIELD_VERSIONS, valueTable = PAUSE_WINDOW_VALUE_VERSIONS } = {}) {
|
|
@@ -15089,6 +15121,10 @@ function parsePauseWindows(text4, path6, { supported = PAUSE_WINDOWS_VERSION, ta
|
|
|
15089
15121
|
} catch (error) {
|
|
15090
15122
|
throw configError4(`pause-windows file is not valid JSON: ${path6} (${error.message})`);
|
|
15091
15123
|
}
|
|
15124
|
+
const duplicate = findDuplicateKey(text4);
|
|
15125
|
+
if (duplicate) {
|
|
15126
|
+
throw configError4(`pause-windows file has a duplicate key ${JSON.stringify(duplicate.key)} at ${duplicate.at}: JSON keeps the LAST value, so the file a person reads and the file that applies are not the same: ${path6}`);
|
|
15127
|
+
}
|
|
15092
15128
|
const problem = pauseWindowsVersionRefusal(parsed, { supported });
|
|
15093
15129
|
if (problem) {
|
|
15094
15130
|
const error = configError4(`${problem.message}: ${path6}`);
|
|
@@ -39837,7 +39873,7 @@ function setMember(members, key2, value) {
|
|
|
39837
39873
|
members[key2] = value;
|
|
39838
39874
|
}
|
|
39839
39875
|
}
|
|
39840
|
-
var MAX_REPORTED_DEPTH, ROOT_SLOT, CREDIT_PER_APPEND, CREDIT_PER_CHARACTER, MAX_PENDING_SCALAR_LENGTH, NUMBER, NUMBER_WITHOUT_EXPONENT_DIGITS, LITERALS, QUOTE, BACKSLASH, StreamingJsonParser, ToolCallArgumentsStream,
|
|
39876
|
+
var MAX_REPORTED_DEPTH, ROOT_SLOT, CREDIT_PER_APPEND, CREDIT_PER_CHARACTER, MAX_PENDING_SCALAR_LENGTH, NUMBER, NUMBER_WITHOUT_EXPONENT_DIGITS, LITERALS, QUOTE, BACKSLASH, StreamingJsonParser, ToolCallArgumentsStream, SIMPLE_ESCAPES;
|
|
39841
39877
|
var init_streaming_json = __esm({
|
|
39842
39878
|
"../node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js"() {
|
|
39843
39879
|
init_json_parse();
|
|
@@ -40030,7 +40066,7 @@ var init_streaming_json = __esm({
|
|
|
40030
40066
|
/** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */
|
|
40031
40067
|
#continueEscape(open2, char) {
|
|
40032
40068
|
if (open2.escape === "\\") {
|
|
40033
|
-
const decoded2 =
|
|
40069
|
+
const decoded2 = SIMPLE_ESCAPES[char];
|
|
40034
40070
|
if (decoded2 !== void 0) {
|
|
40035
40071
|
open2.escape = "";
|
|
40036
40072
|
this.#appendString(open2, decoded2);
|
|
@@ -40234,7 +40270,7 @@ var init_streaming_json = __esm({
|
|
|
40234
40270
|
return this.#arguments;
|
|
40235
40271
|
}
|
|
40236
40272
|
};
|
|
40237
|
-
|
|
40273
|
+
SIMPLE_ESCAPES = {
|
|
40238
40274
|
'"': '"',
|
|
40239
40275
|
"\\": "\\",
|
|
40240
40276
|
"/": "/",
|
|
@@ -114901,10 +114937,12 @@ function withdrawCancel({ redis, jobId, interventionOrigin = "human" }) {
|
|
|
114901
114937
|
Promise.resolve().then(() => redis.del(cancelRequestKeyOf(jobId, interventionOrigin))).catch(() => {
|
|
114902
114938
|
});
|
|
114903
114939
|
}
|
|
114904
|
-
async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)) }) {
|
|
114940
|
+
async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTimeoutMs = 1e4, pollMs = 250, sleep: sleep5 = (ms3) => new Promise((r2) => setTimeout(r2, ms3)), now = Date.now, onWritten = null }) {
|
|
114905
114941
|
const requestKey = cancelRequestKeyOf(jobId, interventionOrigin);
|
|
114906
114942
|
const ackKey = interventionOrigin === "agent" ? agentCancelAckKey(jobId) : cancelAckKey(jobId);
|
|
114907
114943
|
await redis.set(requestKey, interventionOrigin === "agent" ? "agent-cancel" : "operator-cancel", "PX", CANCEL_REQ_TTL_MS);
|
|
114944
|
+
onWritten?.();
|
|
114945
|
+
const deadline = now() + ackTimeoutMs;
|
|
114908
114946
|
for (let waited = 0; ; waited += pollMs) {
|
|
114909
114947
|
let host;
|
|
114910
114948
|
try {
|
|
@@ -114914,7 +114952,7 @@ async function requestCancel({ redis, jobId, interventionOrigin = "human", ackTi
|
|
|
114914
114952
|
throw err;
|
|
114915
114953
|
}
|
|
114916
114954
|
if (host !== null && host !== void 0) return { ack: host };
|
|
114917
|
-
if (waited + pollMs > ackTimeoutMs) break;
|
|
114955
|
+
if (waited + pollMs > ackTimeoutMs || now() >= deadline) break;
|
|
114918
114956
|
await sleep5(pollMs);
|
|
114919
114957
|
}
|
|
114920
114958
|
try {
|
|
@@ -114931,31 +114969,44 @@ function ranBefore(job) {
|
|
|
114931
114969
|
return made === 1 ? "it made 1 attempt before and was waiting to retry; whatever that attempt recorded stays, and cancel writes none" : `it made ${made} attempts before and was waiting to retry; whatever those attempts recorded stays, and cancel writes none`;
|
|
114932
114970
|
}
|
|
114933
114971
|
async function holdInPlace(redis, jobId) {
|
|
114934
|
-
if (typeof redis?.hexists !== "function") return
|
|
114972
|
+
if (typeof redis?.hexists !== "function") return null;
|
|
114935
114973
|
try {
|
|
114936
114974
|
return await redis.hexists(jobKey(jobId), "since") === 1;
|
|
114937
114975
|
} catch {
|
|
114938
|
-
return
|
|
114976
|
+
return null;
|
|
114939
114977
|
}
|
|
114940
114978
|
}
|
|
114979
|
+
function neverSent(err) {
|
|
114980
|
+
return err?.code === "ETIMEDOUT" && err?.message === "Command timed out" && err?.valkeyMayStillRun !== true;
|
|
114981
|
+
}
|
|
114982
|
+
function noAnswer(err) {
|
|
114983
|
+
return err?.code === "ETIMEDOUT" || err?.valkeyMayStillRun === true;
|
|
114984
|
+
}
|
|
114941
114985
|
async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer = false, touchStore = void 0, touchOutbox = null, now = Date.now, interventionOrigin = "human" }) {
|
|
114942
114986
|
let cleared = false;
|
|
114987
|
+
let step = null;
|
|
114943
114988
|
try {
|
|
114944
114989
|
const hash2 = await redis.hgetall(jobKey(jobId));
|
|
114945
114990
|
if (!hash2 || !hash2.since) return { invalid: `job ${jobId} is not waiting on a condition` };
|
|
114946
114991
|
const job = await queue.getJob(jobId);
|
|
114947
114992
|
if (!job) return { invalid: `job ${jobId} is no longer in the queue` };
|
|
114948
|
-
|
|
114993
|
+
let unread2 = null;
|
|
114994
|
+
const state5 = await job.getState().catch((err) => (unread2 = err, null));
|
|
114995
|
+
if (unread2 !== null) return { invalid: `the state of job ${jobId} could not be read, so nothing was changed: ${unread2?.message ?? String(unread2)}`, ...noAnswerOf(unread2) };
|
|
114949
114996
|
if (state5 !== "delayed" && state5 !== "waiting" && state5 !== "prioritized") {
|
|
114950
114997
|
return { invalid: `job ${jobId} is ${state5 ?? "in an unknown state"}, not waiting -- it has already left the hold` };
|
|
114951
114998
|
}
|
|
114952
114999
|
cleared = true;
|
|
115000
|
+
step = "hold";
|
|
114953
115001
|
try {
|
|
114954
115002
|
await redis.del(jobKey(jobId));
|
|
114955
115003
|
} catch (err) {
|
|
114956
|
-
|
|
115004
|
+
const inPlace = await holdInPlace(redis, jobId);
|
|
115005
|
+
if (inPlace === true || neverSent(err)) cleared = false;
|
|
115006
|
+
else if (inPlace === false) step = null;
|
|
114957
115007
|
throw err;
|
|
114958
115008
|
}
|
|
115009
|
+
step = null;
|
|
114959
115010
|
await redis.srem(HELD_SET, jobId).catch(() => {
|
|
114960
115011
|
});
|
|
114961
115012
|
if (hash2.dedupId) {
|
|
@@ -114963,24 +115014,33 @@ async function removeHeldJob({ redis, queue, jobId, timeoutMs = 2e3, dropDefer =
|
|
|
114963
115014
|
if (holder === jobId) await redis.del(leaseKey(hash2.dedupId));
|
|
114964
115015
|
}
|
|
114965
115016
|
const store = touchStore === void 0 ? typeof redis?.eval === "function" ? makeTouchStore({ redis, now }) : null : touchStore;
|
|
114966
|
-
const
|
|
115017
|
+
const remove2 = async () => {
|
|
115018
|
+
step = "job";
|
|
114967
115019
|
await job.remove();
|
|
114968
115020
|
return { ok: true };
|
|
114969
|
-
}
|
|
115021
|
+
};
|
|
115022
|
+
const removal2 = interventionOrigin === "agent" ? await remove2() : await trackTouchAction({ store, outbox: touchOutbox, event: stopTouch(job, queue, jobId, now), act: remove2, succeeded: () => !(job.attemptsMade > 0) && isWorkload(job) });
|
|
115023
|
+
step = null;
|
|
114970
115024
|
await Promise.all([dropDefer ? dropDeferRecord(redis, jobId, timeoutMs) : null, dropAttemptOf(redis, queue, jobId, timeoutMs)]);
|
|
114971
115025
|
return { ok: true, jobId, attemptsMade: Number.isInteger(job.attemptsMade) && job.attemptsMade > 0 ? job.attemptsMade : 0, ...removal2.touchWarning ? { touchWarning: removal2.touchWarning } : {} };
|
|
114972
115026
|
} catch (err) {
|
|
114973
115027
|
const invalid = err?.message ?? String(err);
|
|
114974
|
-
|
|
115028
|
+
if (!cleared) return { invalid, ...noAnswerOf(err) };
|
|
115029
|
+
return { invalid, cleared: true, ...err?.valkeyMayStillRun === true && step !== null ? { unanswered: step } : {} };
|
|
114975
115030
|
}
|
|
114976
115031
|
}
|
|
114977
115032
|
async function removeQueuedJob({ redis, queue, job, jobId, state: state5 = null, timeoutMs = 2e3, guard = null, touchStore = void 0, touchOutbox = null, now = Date.now, interventionOrigin = "human" }) {
|
|
114978
|
-
|
|
115033
|
+
let holdUnread = null;
|
|
115034
|
+
const hash2 = typeof redis?.hgetall !== "function" ? null : await Promise.resolve().then(() => redis.hgetall(jobKey(jobId))).catch((err) => (holdUnread = err, null));
|
|
115035
|
+
if (holdUnread !== null) {
|
|
115036
|
+
const words = holdUnread?.message ?? String(holdUnread);
|
|
115037
|
+
return { invalid: `could not read whether job ${jobId} is held on a wait condition, so nothing was changed: ${words}`, holdUnread: words, ...noAnswerOf(holdUnread) };
|
|
115038
|
+
}
|
|
114979
115039
|
if (hash2?.since) {
|
|
114980
115040
|
const stop3 = await guarded(guard, job);
|
|
114981
115041
|
if (stop3 !== null) return stop3;
|
|
114982
115042
|
const res = await removeHeldJob({ redis, queue, jobId, timeoutMs, dropDefer: true, touchStore, touchOutbox, now, interventionOrigin });
|
|
114983
|
-
if (!res.ok) return res.cleared === true ? { invalid: res.invalid, cleared: true } : { invalid: res.invalid };
|
|
115043
|
+
if (!res.ok) return res.cleared === true ? { invalid: res.invalid, cleared: true, ...res.unanswered ? { unanswered: res.unanswered } : {} } : { invalid: res.invalid, ...res.noAnswer !== void 0 ? { noAnswer: res.noAnswer } : {} };
|
|
114984
115044
|
return { ok: true, held: true, jobId: res.jobId, attemptsMade: res.attemptsMade, ...res.touchWarning ? { touchWarning: res.touchWarning } : {} };
|
|
114985
115045
|
}
|
|
114986
115046
|
if (!job) return { notQueued: true, state: "unknown" };
|
|
@@ -115020,7 +115080,7 @@ async function dropAttemptOf(redis, queue, jobId, timeoutMs) {
|
|
|
115020
115080
|
function dropDeferRecord(redis, jobId, timeoutMs) {
|
|
115021
115081
|
return dropKey(redis, deferKey(jobId), timeoutMs);
|
|
115022
115082
|
}
|
|
115023
|
-
var isWorkload, CANCEL_REQ_TTL_MS, CANCEL_ACK_TTL_MS, cancelReqKey, cancelAckKey, agentCancelReqKey, agentCancelAckKey, cancelRequestKeyOf, QUEUED_STATES2;
|
|
115083
|
+
var isWorkload, CANCEL_REQ_TTL_MS, CANCEL_ACK_TTL_MS, cancelReqKey, cancelAckKey, agentCancelReqKey, agentCancelAckKey, cancelRequestKeyOf, noAnswerOf, QUEUED_STATES2;
|
|
115024
115084
|
var init_cancel_state = __esm({
|
|
115025
115085
|
"../worker/src/cancel-state.mjs"() {
|
|
115026
115086
|
init_touch_state();
|
|
@@ -115037,6 +115097,7 @@ var init_cancel_state = __esm({
|
|
|
115037
115097
|
agentCancelReqKey = (jobId) => `cancel:agent:req:${jobId}`;
|
|
115038
115098
|
agentCancelAckKey = (jobId) => `cancel:agent:ack:${jobId}`;
|
|
115039
115099
|
cancelRequestKeyOf = (jobId, interventionOrigin) => interventionOrigin === "agent" ? agentCancelReqKey(jobId) : cancelReqKey(jobId);
|
|
115100
|
+
noAnswerOf = (err) => noAnswer(err) ? { noAnswer: err?.message ?? String(err) } : {};
|
|
115040
115101
|
QUEUED_STATES2 = Object.freeze(["delayed", "waiting", "prioritized", "paused"]);
|
|
115041
115102
|
}
|
|
115042
115103
|
});
|
|
@@ -117398,10 +117459,12 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
|
|
|
117398
117459
|
const envPath = join8(cwd, ".env");
|
|
117399
117460
|
let content = null;
|
|
117400
117461
|
let error = null;
|
|
117462
|
+
let absent = false;
|
|
117401
117463
|
try {
|
|
117402
117464
|
content = readEnv3(envPath);
|
|
117403
117465
|
} catch (err) {
|
|
117404
|
-
if (err?.code
|
|
117466
|
+
if (err?.code === "ENOENT") absent = true;
|
|
117467
|
+
else error = `${envPath} could not be read (${err?.code ?? err?.message})`;
|
|
117405
117468
|
}
|
|
117406
117469
|
const fileKeys = {};
|
|
117407
117470
|
if (content !== null) {
|
|
@@ -117415,9 +117478,9 @@ function valkeyClientContext({ env: env2 = process.env, cwd = process.cwd(), pla
|
|
|
117415
117478
|
if (keys3.error) error ??= keys3.error;
|
|
117416
117479
|
else for (const k of [VALKEY_SHARED_KEY, VALKEY_PASSWORD_KEY, "VALKEY_URL"]) if (typeof keys3.keys[k] === "string") fileKeys[k] = keys3.keys[k];
|
|
117417
117480
|
}
|
|
117418
|
-
return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, platform, rootRefused });
|
|
117481
|
+
return valkeyContextFromKeys({ env: env2, envPath, fileKeys, error, absent, platform, rootRefused });
|
|
117419
117482
|
}
|
|
117420
|
-
function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKeys = {}, error = null, platform = process.platform, rootRefused } = {}) {
|
|
117483
|
+
function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKeys = {}, error = null, absent = false, platform = process.platform, rootRefused } = {}) {
|
|
117421
117484
|
const fileVenue = typeof fileKeys.PI_BACKENDS === "string" ? { PI_BACKENDS: fileKeys.PI_BACKENDS } : {};
|
|
117422
117485
|
const shared = valkeySharedOn(fileKeys[VALKEY_SHARED_KEY]);
|
|
117423
117486
|
const filePassword = fileKeys[VALKEY_PASSWORD_KEY] || null;
|
|
@@ -117431,7 +117494,7 @@ function valkeyContextFromKeys({ env: env2 = process.env, envPath = null, fileKe
|
|
|
117431
117494
|
const engine = typeof env2.PI_OPENSHELL_ENGINE === "string" ? env2.PI_OPENSHELL_ENGINE : fileKeys.PI_OPENSHELL_ENGINE;
|
|
117432
117495
|
const derived = platform === "linux" && (venues.podmanUsed && !venues.localUsed || openshellSetupOf(venues, { PI_OPENSHELL_ENGINE: engine }).valkeyEngine === "podman");
|
|
117433
117496
|
const envUrl = typeof env2.VALKEY_URL === "string" ? env2.VALKEY_URL : null;
|
|
117434
|
-
return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
|
|
117497
|
+
return { envPath, shared, rootRefused: typeof rootRefused === "boolean" ? rootRefused : derived, error, absent: absent === true, password: { environment: envPassword, file: filePassword }, url: { environment: envUrl, file: fileUrl } };
|
|
117435
117498
|
}
|
|
117436
117499
|
function valkeyUrlProblem(url) {
|
|
117437
117500
|
let u;
|
|
@@ -117570,6 +117633,11 @@ function scrubValkeyError(err) {
|
|
|
117570
117633
|
}
|
|
117571
117634
|
return err;
|
|
117572
117635
|
}
|
|
117636
|
+
function refusedDatabaseOf(client) {
|
|
117637
|
+
if (!client?.[DB_REFUSED]) return null;
|
|
117638
|
+
const db = client.condition?.select ?? client.options?.db;
|
|
117639
|
+
return { db: Number.isSafeInteger(db) ? db : null };
|
|
117640
|
+
}
|
|
117573
117641
|
function isSelectRangeError(err) {
|
|
117574
117642
|
return Boolean(err) && typeof err === "object" && String(err.command?.name ?? "").toLowerCase() === "select" && /DB index is out of range|invalid DB index/i.test(String(err.message ?? ""));
|
|
117575
117643
|
}
|
|
@@ -117593,10 +117661,12 @@ function onValkeyError(emitter, what2, write = (line2) => process.stderr.write(l
|
|
|
117593
117661
|
function givesUp(connection) {
|
|
117594
117662
|
return connection?.[JUDGE]?.failFast === true;
|
|
117595
117663
|
}
|
|
117596
|
-
function parseConnection(url, { failFast = false, servername = null, context = null, judge: judge2 = null, withoutPassword = false, freshContext = false } = {}) {
|
|
117664
|
+
function parseConnection(url, { failFast = false, servername = null, context = null, judge: judge2 = null, withoutPassword = false, freshContext = false, commandTimeoutMs = null } = {}) {
|
|
117597
117665
|
const u = new URL(url);
|
|
117598
117666
|
const dbProblem = valkeyUrlProblem(url);
|
|
117599
117667
|
if (dbProblem) throw valkeyRefusal(dbProblem);
|
|
117668
|
+
const bounded12 = Number.isSafeInteger(commandTimeoutMs) && commandTimeoutMs > 0;
|
|
117669
|
+
if (bounded12 && !failFast) throw new TypeError("parseConnection: a command bound (commandTimeoutMs) is for a failFast client only, which has no offline queue to send a command that timed out from later");
|
|
117600
117670
|
const where = context ?? defaultValkeyContext();
|
|
117601
117671
|
const { password } = valkeyPasswordFor(url, where, { withoutPassword });
|
|
117602
117672
|
return {
|
|
@@ -117605,7 +117675,9 @@ function parseConnection(url, { failFast = false, servername = null, context = n
|
|
|
117605
117675
|
// reports. `context` is where the client stands (`valkeyClientContext`), the process's own when not given.
|
|
117606
117676
|
Connector: JudgedConnector,
|
|
117607
117677
|
// `judge` is a test seam only: the function asked for the address (judgedEndpoint when not given).
|
|
117608
|
-
|
|
117678
|
+
// `freshPassword` (issue #845): a client that judges by the context as it is at each connect sends the password that
|
|
117679
|
+
// context gives then, unless it was built to send none (`withoutPassword`, a probe of what another account sees).
|
|
117680
|
+
[JUDGE]: { url, context: where, ...judge2 ? { judge: judge2 } : {}, ...freshContext && !context ? { freshContext: true, ...withoutPassword ? {} : { freshPassword: true } } : {}, ...failFast ? { failFast: true } : {}, ...bounded12 ? { commandBound: true } : {} },
|
|
117609
117681
|
// WHATWG URL keeps an IPv6 literal's brackets in `hostname` (`[::1]`), and net.connect then looks up the name
|
|
117610
117682
|
// "[::1]", which never resolves, so a `redis://[::1]:6379` VALKEY_URL never connected (issue #464, measured on
|
|
117611
117683
|
// Fedora 44: ETIMEDOUT). The address is the part inside them.
|
|
@@ -117627,7 +117699,10 @@ function parseConnection(url, { failFast = false, servername = null, context = n
|
|
|
117627
117699
|
// don't buffer commands while disconnected -- error now
|
|
117628
117700
|
retryStrategy: (attempts) => attempts > 2 ? null : 200
|
|
117629
117701
|
// give up after ~2 tries
|
|
117630
|
-
} : {}
|
|
117702
|
+
} : {},
|
|
117703
|
+
// ioredis' resend of what was on a connection that died is off with the bound: the caller was told such a command
|
|
117704
|
+
// failed, and a resend on the next connection would make it run after that.
|
|
117705
|
+
...bounded12 ? { commandTimeout: commandTimeoutMs, autoResendUnfulfilledCommands: false } : {}
|
|
117631
117706
|
};
|
|
117632
117707
|
}
|
|
117633
117708
|
function makeRedisClient(url, { servername = null, context = null, failFast = false, lazyConnect = false, judge: judge2 = null, withoutPassword = false, disconnectTimeoutMs = null, noOfflineQueue = false, commandTimeoutMs = null, freshContext = false } = {}) {
|
|
@@ -117635,6 +117710,30 @@ function makeRedisClient(url, { servername = null, context = null, failFast = fa
|
|
|
117635
117710
|
const options = { ...parseConnection(url, { failFast, servername, context, judge: judge2, withoutPassword, freshContext }), ...noOfflineQueue ? { enableOfflineQueue: false, maxRetriesPerRequest: 1 } : {}, ...lazyConnect ? { lazyConnect: true } : {}, ...Number.isSafeInteger(disconnectTimeoutMs) && disconnectTimeoutMs >= 0 ? { disconnectTimeout: disconnectTimeoutMs } : {}, ...bounded12 ? { commandTimeout: commandTimeoutMs, autoResendUnfulfilledCommands: false } : {} };
|
|
117636
117711
|
return bounded12 ? new BoundedRedis(options) : new Redis(options);
|
|
117637
117712
|
}
|
|
117713
|
+
function sameValkeyServer(a, b) {
|
|
117714
|
+
const x2 = valkeyTarget(a);
|
|
117715
|
+
const y = valkeyTarget(b);
|
|
117716
|
+
return !x2.error && !y.error && x2.host === y.host && x2.port === y.port;
|
|
117717
|
+
}
|
|
117718
|
+
function freshPasswordVerdict(judge2, context, held) {
|
|
117719
|
+
if (!context || context.error || context.absent === true) return { take: false, declined: null };
|
|
117720
|
+
if ((context.envPath ?? null) !== (judge2?.context?.envPath ?? null)) return { take: false, declined: null };
|
|
117721
|
+
const { password, from } = valkeyPasswordFor(judge2.url, context);
|
|
117722
|
+
const fromFile2 = from !== "VALKEY_URL" && from !== "the environment";
|
|
117723
|
+
if (fromFile2 && !sameValkeyServer(context.url?.file ?? DEFAULT_VALKEY_URL, judge2.url)) return { take: false, declined: (password ?? null) !== (held ?? null) };
|
|
117724
|
+
return { take: true, password: password ?? null };
|
|
117725
|
+
}
|
|
117726
|
+
function passwordDeclinedOf(client) {
|
|
117727
|
+
const connector = client?.connector;
|
|
117728
|
+
if (connector?.[PASSWORD_DECLINED] !== true) return false;
|
|
117729
|
+
const judge2 = connector.options?.[JUDGE];
|
|
117730
|
+
if (judge2?.freshPassword !== true) return false;
|
|
117731
|
+
try {
|
|
117732
|
+
return freshPasswordVerdict(judge2, defaultValkeyContext(), connector.options.password).declined === true;
|
|
117733
|
+
} catch {
|
|
117734
|
+
return false;
|
|
117735
|
+
}
|
|
117736
|
+
}
|
|
117638
117737
|
function isJudgedConnection(connection) {
|
|
117639
117738
|
const options = connection?.options ?? connection;
|
|
117640
117739
|
return options?.Connector === JudgedConnector && Boolean(options?.[JUDGE]);
|
|
@@ -117665,7 +117764,7 @@ async function claimValkeyOwner(url, folder2, { context = null, waitMs = 15e3, m
|
|
|
117665
117764
|
await sleep5(500);
|
|
117666
117765
|
}
|
|
117667
117766
|
}
|
|
117668
|
-
var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, BoundedRedis, JUDGE, JudgedConnector;
|
|
117767
|
+
var SCRUBS, DB_REFUSED, SELECT_HOOK, VALKEY_ERROR_QUIET_MS, VALKEY_ERRORS_HELD, saidAt, COMMAND_BOUND_HOOK, BoundedRedis, JUDGE, FRESH_PASSWORD, PASSWORD_DECLINED, JudgedConnector, FRESH_PASSWORD_HOOK;
|
|
117669
117768
|
var init_connection = __esm({
|
|
117670
117769
|
"../worker/src/connection.mjs"() {
|
|
117671
117770
|
init_valkey_auth();
|
|
@@ -117717,6 +117816,23 @@ var init_connection = __esm({
|
|
|
117717
117816
|
VALKEY_ERROR_QUIET_MS = 6e4;
|
|
117718
117817
|
VALKEY_ERRORS_HELD = 256;
|
|
117719
117818
|
saidAt = /* @__PURE__ */ new Map();
|
|
117819
|
+
COMMAND_BOUND_HOOK = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-command-bound");
|
|
117820
|
+
if (!Redis.prototype[COMMAND_BOUND_HOOK]) {
|
|
117821
|
+
const sendCommand = Redis.prototype.sendCommand;
|
|
117822
|
+
Redis.prototype.sendCommand = function sendCommandWithBound(command3, stream11) {
|
|
117823
|
+
const answer = sendCommand.call(this, command3, stream11);
|
|
117824
|
+
if (this.options?.[JUDGE]?.commandBound !== true) return answer;
|
|
117825
|
+
command3._commandTimeoutTimer?.unref?.();
|
|
117826
|
+
if (this.commandQueue?.peekBack?.()?.command !== command3) return answer;
|
|
117827
|
+
answer?.catch?.((err) => {
|
|
117828
|
+
if (!err || typeof err !== "object" || err.name === "ReplyError" || err.valkeyRefused) return;
|
|
117829
|
+
if (err.message === "Command timed out") err.code ??= "ETIMEDOUT";
|
|
117830
|
+
err.valkeyMayStillRun = true;
|
|
117831
|
+
});
|
|
117832
|
+
return answer;
|
|
117833
|
+
};
|
|
117834
|
+
Redis.prototype[COMMAND_BOUND_HOOK] = true;
|
|
117835
|
+
}
|
|
117720
117836
|
BoundedRedis = class extends Redis {
|
|
117721
117837
|
sendCommand(command3, stream11) {
|
|
117722
117838
|
const answer = super.sendCommand(command3, stream11);
|
|
@@ -117737,6 +117853,8 @@ var init_connection = __esm({
|
|
|
117737
117853
|
}
|
|
117738
117854
|
};
|
|
117739
117855
|
JUDGE = "piDispatchValkeyJudge";
|
|
117856
|
+
FRESH_PASSWORD = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-fresh-password");
|
|
117857
|
+
PASSWORD_DECLINED = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-password-declined");
|
|
117740
117858
|
JudgedConnector = class _JudgedConnector extends AbstractConnector {
|
|
117741
117859
|
constructor(options) {
|
|
117742
117860
|
super(options.disconnectTimeout);
|
|
@@ -117746,6 +117864,55 @@ var init_connection = __esm({
|
|
|
117746
117864
|
check() {
|
|
117747
117865
|
return !this[DB_REFUSED];
|
|
117748
117866
|
}
|
|
117867
|
+
/**
|
|
117868
|
+
* Issue #845: a `freshPassword` client's password, as `valkeyPasswordFor` reads it from the context as it is now, put
|
|
117869
|
+
* where ioredis reads it for the connect about to start (the hook below). Here and not in `connect()`: by the time
|
|
117870
|
+
* ioredis asks the connector for a stream it has already built that connect's AUTH from `options.password`. The options
|
|
117871
|
+
* are the client's own object (ioredis hands its `options` to the Connector it constructs), so this is what the client
|
|
117872
|
+
* sends. A password from VALKEY_URL's userinfo comes back as it is, so it stays fixed. The last password is kept by a
|
|
117873
|
+
* context that:
|
|
117874
|
+
* - throws, or names an `error` (a `.env` that cannot be read, or a line in it the service's loader reads
|
|
117875
|
+
* differently): it cannot say which password the file holds, and dropping the one that worked would refuse a client
|
|
117876
|
+
* that is fine;
|
|
117877
|
+
* - finds no file (`absent`): a `.env` gone, for good or for the moment an editor replaces it, is not a file that
|
|
117878
|
+
* sets no password, so it does not drop one either (the console says to put the file back before a restart);
|
|
117879
|
+
* - would answer from the `.env` while that file does not name the Valkey this client dials: a password the file
|
|
117880
|
+
* gives (or the lack of one) belongs to the Valkey the file's VALKEY_URL names, the default one when it names
|
|
117881
|
+
* none, and a held client never moves to another Valkey. Review of PR #878, measured with two Valkeys: one
|
|
117882
|
+
* deployment moving to another Valkey changes both lines together, a symlinked deployment folder repointed reads
|
|
117883
|
+
* another deployment's file through the same path, and a shell that exports VALKEY_URL keeps the client on its URL
|
|
117884
|
+
* whatever the file says; in each the new Valkey's password went to the old one, which refused it, and the pages
|
|
117885
|
+
* lost the old Valkey too. The check is against the URL the client dials, not the context's own choice of URL
|
|
117886
|
+
* (`valkeyUrlFor`, which prefers this shell's and so never moved), and by the server each names (`sameValkeyServer`:
|
|
117887
|
+
* `valkeyTarget`'s host and port, so a scheme alias, a database path or a trailing `/` is the same Valkey, while
|
|
117888
|
+
* `localhost` and `127.0.0.1` stay two names, as `valkeyEndpointTag` keeps them). A rotation keeps the file's URL,
|
|
117889
|
+
* so it is taken, and so is a password a bad line hid at start once the line is fixed, when the file names the
|
|
117890
|
+
* client's Valkey. A password declined here that is not the one the client holds is recorded (`PASSWORD_DECLINED`,
|
|
117891
|
+
* `passwordDeclinedOf`), so the console names VALKEY_PASSWORD as needing a restart rather than going on refused in
|
|
117892
|
+
* silence (a shell that exports `redis://localhost:6440` over a file's `redis://127.0.0.1:6440`, say);
|
|
117893
|
+
* - is read from another `.env` path than the one the client was built with: the pi extension's context follows the
|
|
117894
|
+
* deployment pointer, and that alone says the file is another deployment's.
|
|
117895
|
+
* A password from this process's environment, or from VALKEY_URL's own userinfo, is not the file's, and is taken as it
|
|
117896
|
+
* comes. A context that passes all of these and gives no password drops it, as a restart would.
|
|
117897
|
+
*/
|
|
117898
|
+
[FRESH_PASSWORD]() {
|
|
117899
|
+
const judge2 = this.options?.[JUDGE];
|
|
117900
|
+
if (judge2?.freshPassword !== true) return;
|
|
117901
|
+
let context;
|
|
117902
|
+
try {
|
|
117903
|
+
context = defaultValkeyContext();
|
|
117904
|
+
} catch {
|
|
117905
|
+
return;
|
|
117906
|
+
}
|
|
117907
|
+
const verdict2 = freshPasswordVerdict(judge2, context, this.options.password);
|
|
117908
|
+
if (!verdict2.take) {
|
|
117909
|
+
if (verdict2.declined !== null) this[PASSWORD_DECLINED] = verdict2.declined;
|
|
117910
|
+
return;
|
|
117911
|
+
}
|
|
117912
|
+
this[PASSWORD_DECLINED] = false;
|
|
117913
|
+
if (verdict2.password) this.options.password = verdict2.password;
|
|
117914
|
+
else delete this.options.password;
|
|
117915
|
+
}
|
|
117749
117916
|
connect() {
|
|
117750
117917
|
const { options } = this;
|
|
117751
117918
|
if (this[DB_REFUSED]) return Promise.reject(this[DB_REFUSED]);
|
|
@@ -117777,6 +117944,18 @@ var init_connection = __esm({
|
|
|
117777
117944
|
}
|
|
117778
117945
|
};
|
|
117779
117946
|
JudgedConnector.dial = { net: netConnect2, tls: tlsConnect };
|
|
117947
|
+
FRESH_PASSWORD_HOOK = /* @__PURE__ */ Symbol.for("pi-dispatch.valkey-fresh-password-hook");
|
|
117948
|
+
if (!Redis.prototype[FRESH_PASSWORD_HOOK]) {
|
|
117949
|
+
const connect = Redis.prototype.connect;
|
|
117950
|
+
Redis.prototype.connect = function connectWithFreshPassword(...args) {
|
|
117951
|
+
try {
|
|
117952
|
+
this.connector?.[FRESH_PASSWORD]?.();
|
|
117953
|
+
} catch {
|
|
117954
|
+
}
|
|
117955
|
+
return connect.apply(this, args);
|
|
117956
|
+
};
|
|
117957
|
+
Redis.prototype[FRESH_PASSWORD_HOOK] = true;
|
|
117958
|
+
}
|
|
117780
117959
|
}
|
|
117781
117960
|
});
|
|
117782
117961
|
|
|
@@ -118870,6 +119049,10 @@ var init_redact_secrets = __esm({
|
|
|
118870
119049
|
NON_SECRET_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
118871
119050
|
// Issue #693: a memory-derived concurrency count, never a credential; keep it in the structural venue env.
|
|
118872
119051
|
"PI_CODEMODE_MAX_WORKERS",
|
|
119052
|
+
// Issue #865: the job's size, two integers, never a secret. Listed, or OpenShell would put each in the job's credential
|
|
119053
|
+
// provider and the agent would read a placeholder where a tool's worker count needs the number.
|
|
119054
|
+
"PI_JOB_SIZE_CPUS",
|
|
119055
|
+
"PI_JOB_SIZE_MEMORY_MIB",
|
|
118873
119056
|
"PI_PROVIDER",
|
|
118874
119057
|
"PI_MODEL",
|
|
118875
119058
|
"PI_FORK_ENTRY",
|
|
@@ -121668,7 +121851,7 @@ function hostsAgree(rows3) {
|
|
|
121668
121851
|
const [first, ...others] = rows3.map(loadsFingerprint);
|
|
121669
121852
|
return others.every((other) => other === first);
|
|
121670
121853
|
}
|
|
121671
|
-
var HOST_LOADS_CAPS, HOST_LOADS_MAX_BYTES, FIT_ORDER, HOST_LOADS_PARTS, HOST_LOADS_MORE, HOST_LOADS_LISTS, LOADS_EXTENSION_KINDS, LOADS_TRANSPORTS, LOADS_FLOW_TIERS, LOADS_REPO_STATES, isObject4, LOADS_IGNORE_FILES;
|
|
121854
|
+
var HOST_LOADS_CAPS, HOST_LOADS_MAX_BYTES, FIT_ORDER, HOST_LOADS_PARTS, HOST_LOADS_MORE, HOST_LOADS_LISTS, LOADS_EXTENSION_KINDS, LOADS_TRANSPORTS, LOADS_FLOW_TIERS, LOADS_REPO_STATES, isObject4, FLOW_INPUT_FIELDS, FLOW_BASIS_HEX, FLOW_BASIS_RE, LOADS_IGNORE_FILES;
|
|
121672
121855
|
var init_host_loads = __esm({
|
|
121673
121856
|
"../worker/src/host-loads.mjs"() {
|
|
121674
121857
|
init_doctor_results();
|
|
@@ -121688,6 +121871,9 @@ var init_host_loads = __esm({
|
|
|
121688
121871
|
LOADS_FLOW_TIERS = Object.freeze(["repository", "trigger", "overlay", "package", "none", "unknown"]);
|
|
121689
121872
|
LOADS_REPO_STATES = Object.freeze(["present", "absent", "unknown"]);
|
|
121690
121873
|
isObject4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
121874
|
+
FLOW_INPUT_FIELDS = Object.freeze(["flow", "folder", "skillsDir", "packages", "excludeExtensions"]);
|
|
121875
|
+
FLOW_BASIS_HEX = 8;
|
|
121876
|
+
FLOW_BASIS_RE = new RegExp(`^[0-9a-f]{${FLOW_BASIS_HEX}}$`);
|
|
121691
121877
|
LOADS_IGNORE_FILES = Object.freeze([".gitignore", ".ignore", ".fdignore"]);
|
|
121692
121878
|
}
|
|
121693
121879
|
});
|
|
@@ -121881,7 +122067,7 @@ var init_mcp_preflight = __esm({
|
|
|
121881
122067
|
"mcp-list-invalid": "the list this job carries is not one the triggers file's loader writes (a service in this deployment may be at another version)",
|
|
121882
122068
|
"mcp-config-invalid": "this deployment's list of MCP servers (mcp.json in the overlay) cannot be used",
|
|
121883
122069
|
"mcp-server-unknown": "this deployment does not offer that server",
|
|
121884
|
-
"mcp-secret-reserved": "that server would be handed a
|
|
122070
|
+
"mcp-secret-reserved": "that server would be handed a variable this deployment keeps for the job itself",
|
|
121885
122071
|
"mcp-secret-unmapped": "a credential that server needs is not passed by this trigger",
|
|
121886
122072
|
"mcp-excluded": "this trigger's run.excludeTools removes every way the job could reach that server",
|
|
121887
122073
|
"mcp-egress-unreachable": "the egress policy does not let a job reach that server",
|
|
@@ -122136,7 +122322,7 @@ var init_reason_catalog = __esm({
|
|
|
122136
122322
|
["mcp-config-invalid", "Refused: the list of MCP servers could not be used: the overlay `mcp.json` before the job started, or the list the worker wrote for the job inside its container. Not run.", "Fix the overlay `mcp.json` (`pi-dispatch doctor` checks it), and run a worker and a job image of the same release."],
|
|
122137
122323
|
["mcp-server-unknown", mcpRefusalComment("mcp-server-unknown", null), "Declare the server in the overlay `mcp.json`, or fix the name in `run.mcp`."],
|
|
122138
122324
|
["mcp-secret-unmapped", mcpRefusalComment("mcp-secret-unmapped", null), "Pass the credential the server needs in the trigger's `run.secrets`."],
|
|
122139
|
-
["mcp-secret-reserved", mcpRefusalComment("mcp-secret-reserved", null), "Give the server
|
|
122325
|
+
["mcp-secret-reserved", mcpRefusalComment("mcp-secret-reserved", null), "Give the server a variable of its own, not one the job keeps."],
|
|
122140
122326
|
["mcp-excluded", mcpRefusalComment("mcp-excluded", null), "Remove the server from `run.mcp`, or keep a tool that reaches it out of `run.excludeTools`."],
|
|
122141
122327
|
["mcp-egress-unreachable", mcpRefusalComment("mcp-egress-unreachable", null), "Add the server's host to the egress allowlist, or drop the server from `run.mcp`."],
|
|
122142
122328
|
["mcp-package-runner", mcpRefusalComment("mcp-package-runner", null), "Allow the package registry in the egress allowlist, or install the server in the job image."],
|
|
@@ -122801,7 +122987,7 @@ function triggerLastRun(records, hostLoads = null) {
|
|
|
122801
122987
|
...sourcesOf(loaded, "package").filter((row2) => row2.toolNames?.length > 0).map((row2) => ({ kind: "package", from: row2.name ?? "a staged package", names: [...row2.toolNames] })),
|
|
122802
122988
|
...(overlay?.namedTools ?? []).map((row2) => ({ kind: "overlay", from: row2.name, names: [...row2.tools] }))
|
|
122803
122989
|
];
|
|
122804
|
-
const sources = [...sourcesOf(loaded, "package"), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
|
|
122990
|
+
const sources = [...sourcesOf(loaded, "package").map((row2) => ({ ...row2, kind: "package" })), ...(overlay?.names ?? []).map((name2) => ({ name: name2, version: null, kind: "overlay", tools: overlay.namedTools?.find((row2) => row2.name === name2)?.tools.length ?? 0, skills: 0, prompts: 0 }))];
|
|
122805
122991
|
return {
|
|
122806
122992
|
newest,
|
|
122807
122993
|
quoted: { id: runIdOf2(record3), host: facts7.host, record: record3, rows: Object.fromEntries(loadedRows(record3, { past: true }).map((row2) => [row2.label, row2])), problems, sources, tools, notes: [...cutWords(loaded), ...wentOn(record3) && loaded.at !== "ready" ? [LOADED_WORDS.notedEarly] : []] }
|
|
@@ -122829,12 +123015,12 @@ function hostLoadsRow(hostLoads, name2) {
|
|
|
122829
123015
|
const row2 = Array.isArray(hostLoads?.hosts) ? hostLoads.hosts.find((entry3) => entry3?.name === name2) : null;
|
|
122830
123016
|
return row2 ?? { name: name2, state: "unknown", why: hostLoads?.unreachable ? "unreachable" : "absent" };
|
|
122831
123017
|
}
|
|
122832
|
-
function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null }) {
|
|
123018
|
+
function sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages = false, excludes = [], image = null, mcp = null }) {
|
|
122833
123019
|
const row2 = hostLoadsRow(hostLoads, host);
|
|
122834
123020
|
const first = noOverlay(hostPiFor(row2, image));
|
|
122835
123021
|
const unknown3 = (why) => [`${first} What a job of trigger ${triggerIndex + 1} would load on this host is unknown: ${why}.`];
|
|
122836
123022
|
if (row2.state === "unknown") return unknown3(row2.why === "unreachable" ? "the hosts\u2019 doctor runs could not be read" : row2.why === "unreadable" ? "its last doctor run could not be read" : "it has published no doctor run");
|
|
122837
|
-
const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed));
|
|
123023
|
+
const unread2 = (row2.unread ?? []).filter((part) => (part !== "packages" || !withholdsPackages) && (part !== "extensions" || row2.overlayExtensionsAllowed) && (part !== "mcp" || !Array.isArray(mcp) || mcp.length > 0));
|
|
122838
123024
|
if (unread2.length > 0) return unknown3(`its last doctor run could not read ${listed(unread2.map((part) => UNREAD_PART_WORDS[part] ?? "part of what it publishes"))}`);
|
|
122839
123025
|
const packages = withholdsPackages ? [] : (row2.packages ?? []).filter((entry3) => !excludes.includes(entry3.name)).map((entry3) => `${entry3.name} ${entry3.version}`);
|
|
122840
123026
|
const extensions = row2.overlayExtensionsAllowed ? (row2.extensions ?? []).map((entry3) => entry3.name).filter((name2) => !excludes.some((excluded) => overlayEntryNamed(excluded, name2))) : [];
|
|
@@ -123824,11 +124010,11 @@ function readDeploymentEnv({ dir, fs: fs9, uid = process.geteuid?.(), names: nam
|
|
|
123824
124010
|
}
|
|
123825
124011
|
}
|
|
123826
124012
|
function deploymentServiceEnv({ env: env2, dir, keys: keys3, platform = process.platform, fs: fs9, uid = process.geteuid?.() }) {
|
|
123827
|
-
const { path: path6, bytes, unreadable: unreadable3, untrusted } = readDeploymentEnv({ dir, fs: fs9, uid });
|
|
124013
|
+
const { path: path6, bytes, absent, unreadable: unreadable3, untrusted } = readDeploymentEnv({ dir, fs: fs9, uid });
|
|
123828
124014
|
const raw = untrusted ? null : bytes;
|
|
123829
124015
|
const file = raw === null ? null : serviceEnvFileOf(raw, path6, serviceEnvLoader(platform));
|
|
123830
124016
|
const shellRunsLine = file?.loader === "shell" && file.hazard !== null && file.hazard.shape !== "shell-crlf-comment" ? file.hazard.line : null;
|
|
123831
|
-
return { ...resolveServiceEnv({ env: env2, file, keys: keys3 }), path: path6, unreadable: unreadable3, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
|
|
124017
|
+
return { ...resolveServiceEnv({ env: env2, file, keys: keys3 }), path: path6, absent, unreadable: unreadable3, untrusted, hazard: file?.hazard ?? null, shellRunsLine };
|
|
123832
124018
|
}
|
|
123833
124019
|
var init_service_env = __esm({
|
|
123834
124020
|
"../worker/src/service-env.mjs"() {
|
|
@@ -125087,6 +125273,8 @@ async function enqueueTriggerFire(plan, options = {}) {
|
|
|
125087
125273
|
const canary = options.packageCanary ?? null;
|
|
125088
125274
|
const id7 = (canary ? manualCanaryJobId : manualTriggerJobId)({ triggerId: current.schedulerId, now: instant7 });
|
|
125089
125275
|
redis = redisFn(url);
|
|
125276
|
+
redis.on?.("error", () => {
|
|
125277
|
+
});
|
|
125090
125278
|
if (await redis.set(`trigger-fire:${current.queue}:${id7}`, "1", "EX", 120, "NX") !== "OK") return { refusal: "This cron trigger was already fired this minute. Nothing was queued.", deduplicated: true };
|
|
125091
125279
|
queue = queueFn(parseConnectionFn(url, { failFast: true }), { name: current.queue });
|
|
125092
125280
|
const schedule = canary ? { ...current.schedule, data: withJobDataVersion({ ...current.schedule.data, packageCanary: canary }) } : current.schedule;
|
|
@@ -127148,6 +127336,7 @@ __export(read_model_exports, {
|
|
|
127148
127336
|
liveAbsentCause: () => liveAbsentCause,
|
|
127149
127337
|
liveJobsOfRun: () => liveJobsOfRun,
|
|
127150
127338
|
loaderRefuses: () => loaderRefuses,
|
|
127339
|
+
loaderTriggers: () => loaderTriggers,
|
|
127151
127340
|
makeLocalRunIndex: () => makeLocalRunIndex,
|
|
127152
127341
|
mapSchedulers: () => mapSchedulers,
|
|
127153
127342
|
mergedDollarProblem: () => mergedDollarProblem,
|
|
@@ -127294,7 +127483,7 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
127294
127483
|
import { homedir as homedir3 } from "node:os";
|
|
127295
127484
|
function resolvePaths(env2 = process.env) {
|
|
127296
127485
|
return {
|
|
127297
|
-
valkeyUrl: env2.VALKEY_URL ??
|
|
127486
|
+
valkeyUrl: env2.VALKEY_URL ?? DEFAULT_VALKEY_URL,
|
|
127298
127487
|
logsDir: logsDirPath(env2),
|
|
127299
127488
|
settingsFile: settingsFilePath(env2),
|
|
127300
127489
|
// Cwd defaults match what `pi-dispatch init` scaffolds (and, since issue #80, what the receiver
|
|
@@ -127454,7 +127643,7 @@ function panelValkeyContext({ env: env2 = process.env, pointerDir = null, owned
|
|
|
127454
127643
|
if (pointerDir === null) return valkeyContextFromKeys({ env: own2, platform });
|
|
127455
127644
|
const res = deploymentServiceEnv({ env: {}, dir: pointerDir, keys: VALKEY_CONTEXT_KEYS, platform, fs: fs9, uid });
|
|
127456
127645
|
const error = res.untrusted ? `${res.path} ${res.untrusted}` : res.unreadable ? `${res.path} could not be read (${res.unreadable})` : res.hazardSkipped.length > 0 ? `${res.path} line ${res.hazard.line} ${res.hazard.what}` : res.unread.length > 0 ? `${res.path} assigns ${res.unread.map((u) => `${u.key} (line ${u.line})`).join(", ")} in a form the service's loader may read differently` : null;
|
|
127457
|
-
return valkeyContextFromKeys({ env: own2, envPath: res.path, fileKeys: res.fromFile, error, platform });
|
|
127646
|
+
return valkeyContextFromKeys({ env: own2, envPath: res.path, fileKeys: res.fromFile, error, absent: res.absent === true, platform });
|
|
127458
127647
|
}
|
|
127459
127648
|
function installPanelValkeyContext({ env: env2 = process.env } = {}) {
|
|
127460
127649
|
useValkeyContext(() => {
|
|
@@ -127581,6 +127770,8 @@ async function checkDispatchRun({
|
|
|
127581
127770
|
let redis;
|
|
127582
127771
|
try {
|
|
127583
127772
|
redis = redisFn(valkeyUrl);
|
|
127773
|
+
redis.on?.("error", () => {
|
|
127774
|
+
});
|
|
127584
127775
|
const key2 = hourKey(now());
|
|
127585
127776
|
const count7 = Number(consumeRate ? await redis.incr(key2) : await redis.get(key2) ?? 0);
|
|
127586
127777
|
if (consumeRate && count7 === 1) await redis.expire(key2, DISPATCH_RUN_TTL_SECONDS);
|
|
@@ -128454,6 +128645,8 @@ async function readHosts({ url, redisFn = makeRedisClient, timeoutMs = 2500, now
|
|
|
128454
128645
|
try {
|
|
128455
128646
|
parseConnection(url, { failFast: true });
|
|
128456
128647
|
redis = redisFn(url);
|
|
128648
|
+
redis.on?.("error", () => {
|
|
128649
|
+
});
|
|
128457
128650
|
const res = await withTimeout2(readLiveHosts(redis, { now, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) }), timeoutMs, { unreachable: "timed out reaching the registry" });
|
|
128458
128651
|
if (res.unreachable) return { unreachable: res.unreachable };
|
|
128459
128652
|
return { hosts: res.hosts };
|
|
@@ -128477,6 +128670,8 @@ async function readHeldJobs({ url, limit: limit3 = 20, redisFn = makeRedisClient
|
|
|
128477
128670
|
try {
|
|
128478
128671
|
parseConnection(url, { failFast: true });
|
|
128479
128672
|
redis = redisFn(url);
|
|
128673
|
+
redis.on?.("error", () => {
|
|
128674
|
+
});
|
|
128480
128675
|
const settled = (async () => {
|
|
128481
128676
|
const ids = await redis.smembers(HELD_SET);
|
|
128482
128677
|
const hydrated = await Promise.all(
|
|
@@ -128545,6 +128740,8 @@ async function cancelHeldJob({ url, jobId, redisFn = makeRedisClient, queueFn =
|
|
|
128545
128740
|
try {
|
|
128546
128741
|
parseConnection(url, { failFast: true });
|
|
128547
128742
|
redis = redisFn(url);
|
|
128743
|
+
redis.on?.("error", () => {
|
|
128744
|
+
});
|
|
128548
128745
|
settled = (async () => {
|
|
128549
128746
|
const { names: names2 } = await readFleetQueues({ url, redisFn, timeoutMs: Math.max(250, Math.floor(timeoutMs / 2)) });
|
|
128550
128747
|
let owner2 = null;
|
|
@@ -128677,6 +128874,8 @@ async function readBudget({ url, redisFn = makeRedisClient, timeoutMs = 2500, no
|
|
|
128677
128874
|
try {
|
|
128678
128875
|
parseConnection(url, { failFast: true });
|
|
128679
128876
|
redis = redisFn(url);
|
|
128877
|
+
redis.on?.("error", () => {
|
|
128878
|
+
});
|
|
128680
128879
|
const settled = Promise.all([redis.get(dayKey(now)), redis.get(weekKey(now)), redis.get(monthKey(now)), redis.get(tokenDayKey(now))]).then(
|
|
128681
128880
|
([day2, week, month, tokens]) => ({ day: Number(day2 ?? 0), week: Number(week ?? 0), month: Number(month ?? 0), tokensToday: Number(tokens ?? 0) }),
|
|
128682
128881
|
(err) => ({ unreachable: err?.message ?? String(err) })
|
|
@@ -128854,11 +129053,13 @@ function readTriggers({ triggersPath, fs: fs9 = nodeFs11 }) {
|
|
|
128854
129053
|
return { triggers: triggers2, count: entries.length, ...bundles !== null && { secretBundles: bundles }, ...loaderRefuses(text4, triggersPath) && { doesNotLoad: true } };
|
|
128855
129054
|
}
|
|
128856
129055
|
function loaderRefuses(text4, path6) {
|
|
129056
|
+
return loaderTriggers(text4, path6) === null;
|
|
129057
|
+
}
|
|
129058
|
+
function loaderTriggers(text4, path6) {
|
|
128857
129059
|
try {
|
|
128858
|
-
parseTriggers(text4, path6);
|
|
128859
|
-
return false;
|
|
129060
|
+
return parseTriggers(text4, path6);
|
|
128860
129061
|
} catch {
|
|
128861
|
-
return
|
|
129062
|
+
return null;
|
|
128862
129063
|
}
|
|
128863
129064
|
}
|
|
128864
129065
|
function configLocationsOf({ paths = {}, deploymentDir = null, home = homedir3() } = {}) {
|
|
@@ -130598,6 +130799,21 @@ function createFleetReader(paths, {
|
|
|
130598
130799
|
async sandboxEnd(request2) {
|
|
130599
130800
|
return requestSandboxEndFn({ redis, ...request2, now: nowFn });
|
|
130600
130801
|
},
|
|
130802
|
+
/**
|
|
130803
|
+
* Issue #845: the database this reader's held client was refused (`refusedDatabaseOf`: `{ db }`, or null when it holds
|
|
130804
|
+
* no refusal). The queues are rebuilt and try again; the held client never does, so its reads stay refused until the
|
|
130805
|
+
* process is restarted, and the console says that rather than only that Valkey could not be read. Reads no Valkey.
|
|
130806
|
+
*/
|
|
130807
|
+
refusedDatabase() {
|
|
130808
|
+
return refusedDatabaseOf(redis);
|
|
130809
|
+
},
|
|
130810
|
+
/**
|
|
130811
|
+
* Issue #845, review of PR #878: whether this reader's held client last declined a VALKEY_PASSWORD the `.env` now
|
|
130812
|
+
* gives, because the file does not name the Valkey it dials as it dials it (`passwordDeclinedOf`). Reads no Valkey.
|
|
130813
|
+
*/
|
|
130814
|
+
passwordDeclined() {
|
|
130815
|
+
return passwordDeclinedOf(redis);
|
|
130816
|
+
},
|
|
130601
130817
|
async dispose() {
|
|
130602
130818
|
disposed = true;
|
|
130603
130819
|
for (const q of sandboxQueues.values()) {
|
|
@@ -138104,7 +138320,10 @@ async function readConnections({ deploymentDir, env: env2 = {}, paths = {}, snap
|
|
|
138104
138320
|
if (redis) return redis;
|
|
138105
138321
|
if (owned) return owned;
|
|
138106
138322
|
if (injectedRead && typeof paths.valkeyUrl !== "string") return null;
|
|
138107
|
-
|
|
138323
|
+
owned = redisFn(paths.valkeyUrl);
|
|
138324
|
+
owned.on?.("error", () => {
|
|
138325
|
+
});
|
|
138326
|
+
return owned;
|
|
138108
138327
|
};
|
|
138109
138328
|
const endpointFleetDeclared = endpointFleetDeclaration(file, environmentUnknown);
|
|
138110
138329
|
let endpointStatus = { state: "unknown", hosts: [], more: 0, partial: true };
|
|
@@ -150359,9 +150578,15 @@ function buildSandboxRunArgs({ venue = DEFAULT_BACKEND, image, name: name2, work
|
|
|
150359
150578
|
TMOUT: idleSeconds > 0 ? String(idleSeconds) : void 0,
|
|
150360
150579
|
// Beside `--user` only (issue #341). Not a credential either.
|
|
150361
150580
|
HOME: user !== null ? home : void 0,
|
|
150581
|
+
// Issue #865: the size this shell is built at (`size`: the manifest's, the run's own or, for a prepared
|
|
150582
|
+
// sandbox, its trigger's), by the job env's own function (`jobSizeEnv`), so a flow command copied in here
|
|
150583
|
+
// (`jest --maxWorkers=$PI_JOB_SIZE_CPUS`) sizes its pool as a job of that size would instead of expanding to
|
|
150584
|
+
// nothing. A run retained before this release never had the variables; its shell gets them for its recorded
|
|
150585
|
+
// size all the same. Two integers, not a credential.
|
|
150586
|
+
...jobSizeEnv(size),
|
|
150362
150587
|
// Still NO CREDENTIALS, and that clause is untouched: a proxy URL is not a credential, and
|
|
150363
150588
|
// buildContainerEnv is still not reused here. The env is two variables about the terminal, HOME
|
|
150364
|
-
// beside `--user`, and, when a policy is armed, four about the network.
|
|
150589
|
+
// beside `--user`, two about the run's size and, when a policy is armed, four about the network.
|
|
150365
150590
|
...proxyEnv
|
|
150366
150591
|
},
|
|
150367
150592
|
// Ahead of the env and the mounts, and well ahead of the image, which the builder keeps as
|
|
@@ -153083,7 +153308,7 @@ function sizingLines(sizing, id7, iw, styler) {
|
|
|
153083
153308
|
if (verdicts.length > 0) push(wrapColumns(verdicts.join("; "), sub2, styler), tone);
|
|
153084
153309
|
if (typeof s2.call === "string") out.push(...callLines(cellOf(s2.call), iw, styler, indent.length));
|
|
153085
153310
|
if (over) push(wrapColumns(`no live host admits it (${cellOf(s2.words?.refusal ?? "")}): a job of it would wait for a host that never comes`, sub2, styler), "error");
|
|
153086
|
-
for (const fact of [s2.words?.memoryFact, s2.words?.cpuFact]) if (typeof fact === "string" && fact !== "") push(wrapColumns(cellOf(fact), sub2, styler), "dim");
|
|
153311
|
+
for (const fact of [s2.words?.memoryFact, s2.words?.cpuFact, s2.words?.memoryCeiling]) if (typeof fact === "string" && fact !== "") push(wrapColumns(cellOf(fact), sub2, styler), "dim");
|
|
153087
153312
|
return out;
|
|
153088
153313
|
}
|
|
153089
153314
|
function projectRows(snapshot, info = null) {
|
|
@@ -161477,362 +161702,6 @@ var init_models_view = __esm({
|
|
|
161477
161702
|
}
|
|
161478
161703
|
});
|
|
161479
161704
|
|
|
161480
|
-
// src/console/panels/sandboxes.mjs
|
|
161481
|
-
function state2(v) {
|
|
161482
|
-
const s2 = report(v)?.request;
|
|
161483
|
-
if (!s2) return null;
|
|
161484
|
-
const success = s2.state === "ready";
|
|
161485
|
-
return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
|
|
161486
|
-
}
|
|
161487
|
-
function sandboxFromRun(v) {
|
|
161488
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
161489
|
-
const all = rows(v);
|
|
161490
|
-
const id7 = v.query?.id;
|
|
161491
|
-
const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
|
|
161492
|
-
const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
|
|
161493
|
-
const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
|
|
161494
|
-
if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
|
|
161495
|
-
if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
|
|
161496
|
-
const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
|
|
161497
|
-
const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
|
|
161498
|
-
const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
|
|
161499
|
-
const what2 = facts([["Image", picked.image ?? "As recorded in its kept manifest"], ["/workspace", workspace], ["/job", "Read only, with the retained job inputs"], ["pi and what it loads", wordsHtml(loads)], ["Processes", "None until you start them"], ["Credentials", "None, on purpose"], ["Network", "The host's current job egress policy"]], { cols: 1 });
|
|
161500
|
-
return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
|
|
161501
|
-
}
|
|
161502
|
-
function sandboxNew(v) {
|
|
161503
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
161504
|
-
const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
|
|
161505
|
-
const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
|
|
161506
|
-
const triggers2 = options;
|
|
161507
|
-
const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
|
|
161508
|
-
const fields3 = [
|
|
161509
|
-
field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
|
|
161510
|
-
field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
|
|
161511
|
-
field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
|
|
161512
|
-
field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
|
|
161513
|
-
field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
|
|
161514
|
-
];
|
|
161515
|
-
const plan = data5?.plan;
|
|
161516
|
-
const source2 = data5?.fromRun ? html`<p class="sx-p">Preparing from <a class="link" href="${`/run?${new URLSearchParams({ id: data5.fromRun })}`}">${data5.fromRun}</a>. The ref starts at HEAD because the record holds no created pull request. ${data5.sourceRun?.historicalIndex !== null && data5.sourceRun?.historicalIndex !== void 0 ? `Its historical trigger row was ${data5.sourceRun.historicalIndex + 1}; that does not prove the current row's identity.` : "Its original trigger identity is unavailable."} Choose the current trigger explicitly.${String(data5.sourceRun?.backend).startsWith("openshell") ? " Its original OpenShell venue cannot prepare a sandbox; deliberately choose a compatible local or podman trigger." : ""}</p>` : null;
|
|
161517
|
-
const form = html`${source2}<form method="get" action="/runs/sandboxes" data-live-hold>${data5?.fromRun ? html`<input type="hidden" name="fromRun" value="${data5.fromRun}">` : null}<div class="sx-form">${fields3}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
|
|
161518
|
-
const fresh = plan ? html`${facts([[`On ${plan.host}`, wordsHtml(sandboxFreshLine({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }))]], { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
|
|
161519
|
-
const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts5(plan)}${fresh}<p class="sx-p">${plan.pin ? `Pinned for ${plan.pinDays} days` : `Kept for ${plan.retentionHours} hours`}, on ${plan.host}. The worker clones on that host with a credential for this repository, then drops it. No container starts, no model runs, no dollars are spent and no run slot is taken.</p>${acts2(v) ? actForm({ action: "sandbox-prepare", hidden: [["repo", s2.repo], ["index", s2.index, "int"], ["ref", s2.ref], ["host", s2.host], ["pin", Boolean(s2.pin), "json"]], label: "Preview preparation", kind: ["primary"], icon: "plus" }) : html`<p class="card__sub">This console is read-only. Prepare from the host's terminal.</p>`}${cmd(["pi-dispatch", "sandbox", "--new", s2.repo, "--ref", s2.ref, "--as", String(s2.index), ...s2.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s2.index + 1} is index ${s2.index}.</p>` : html`<p class="sx-p" role="status">${data5?.refusal ?? "Choose a repository, its trigger, a ref and a host, then review their settings."}</p>`;
|
|
161520
|
-
return card3("Without a run", html`${form}${preparation}<noscript><p class="card__sub">${SANDBOX_WORDS.noScript}</p></noscript>`, "a fresh clone with a trigger's image and a job's isolation");
|
|
161521
|
-
}
|
|
161522
|
-
function sandboxOpen(v) {
|
|
161523
|
-
if (!allowed3(v)) return null;
|
|
161524
|
-
const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
|
|
161525
|
-
const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
|
|
161526
|
-
const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
|
|
161527
|
-
if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
|
|
161528
|
-
return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
|
|
161529
|
-
}
|
|
161530
|
-
function sandboxKept(v) {
|
|
161531
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
161532
|
-
const data5 = report(v), all = rows(v);
|
|
161533
|
-
return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
|
|
161534
|
-
}
|
|
161535
|
-
function sandboxBrowser(v) {
|
|
161536
|
-
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
161537
|
-
const gateway = v.data?.browserGateways;
|
|
161538
|
-
const count7 = gateway?.enabledHosts?.length ?? 0;
|
|
161539
|
-
return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
|
|
161540
|
-
}
|
|
161541
|
-
var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
|
|
161542
|
-
var init_sandboxes = __esm({
|
|
161543
|
-
"src/console/panels/sandboxes.mjs"() {
|
|
161544
|
-
init_html();
|
|
161545
|
-
init_kit();
|
|
161546
|
-
init_forges();
|
|
161547
|
-
init_sandbox_links();
|
|
161548
|
-
init_common();
|
|
161549
|
-
init_loaded_words();
|
|
161550
|
-
init_loaded_parts();
|
|
161551
|
-
SANDBOX_WORDS = Object.freeze({
|
|
161552
|
-
unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
|
|
161553
|
-
empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
|
|
161554
|
-
noShells: "No live sandbox shell was registered. A shell whose terminal stopped answering disappears when its short lease expires. An expired registration is not proof the container stopped; inspect its host if the terminal ended unexpectedly.",
|
|
161555
|
-
browser: "A separate gateway can attach to an already-open sandbox. It is off by default and enabled by hand per host. Open in Terminal and copy-command support remain available.",
|
|
161556
|
-
noScript: "Preparing, pinning, ending a shell and saving terminal targets require JavaScript for Preview and Confirm. Commands remain selectable and the settings form works without JavaScript."
|
|
161557
|
-
});
|
|
161558
|
-
report = (v) => v.data?.sandboxes;
|
|
161559
|
-
allowed3 = (v) => v.role === "operator";
|
|
161560
|
-
acts2 = (v) => allowed3(v) && !v.readOnly;
|
|
161561
|
-
rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
|
|
161562
|
-
card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
|
|
161563
|
-
stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
|
|
161564
|
-
rowId2 = (r2) => r2.id ?? r2.jobId;
|
|
161565
|
-
canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
|
|
161566
|
-
selected3 = (v) => report(v)?.selected ?? {};
|
|
161567
|
-
facts5 = (plan) => facts([
|
|
161568
|
-
["Image", plan.image ?? "not published"],
|
|
161569
|
-
["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
|
|
161570
|
-
["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
|
|
161571
|
-
["Credentials", "None in the shell"]
|
|
161572
|
-
]);
|
|
161573
|
-
links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
|
|
161574
|
-
retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
|
|
161575
|
-
pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
|
|
161576
|
-
SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
|
|
161577
|
-
}
|
|
161578
|
-
});
|
|
161579
|
-
|
|
161580
|
-
// src/zero-touch-view.mjs
|
|
161581
|
-
function breakdown(row2, repository = false) {
|
|
161582
|
-
const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
|
|
161583
|
-
if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
|
|
161584
|
-
return out;
|
|
161585
|
-
}
|
|
161586
|
-
function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
|
|
161587
|
-
const observed = raw?.coverage ?? raw?.window;
|
|
161588
|
-
const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
|
|
161589
|
-
if (!raw || typeof raw !== "object" || raw.state === "unavailable" || unobserved2) return { state: "unavailable", nowMs: nowMs2, windowDays, coverage: { partial: true, reasons: [] }, totals: {}, projects: [], causes: causes([]), trend: [], groups: [], fixes: [], fixesPartial: true, touchedRunIds: [], waitingRuns: [], ungroupedRunIds: [], grouping: { enabled: false, pending: 0, unassigned: 0, state: "disabled", sources: [] } };
|
|
161590
|
-
const totals = raw.totals ?? raw.summary ?? {};
|
|
161591
|
-
const coverage2 = raw.coverage ?? raw.window ?? {};
|
|
161592
|
-
const out = {
|
|
161593
|
-
state: "ready",
|
|
161594
|
-
nowMs: number3(raw.nowMs) ?? nowMs2,
|
|
161595
|
-
windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
|
|
161596
|
-
coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
|
|
161597
|
-
totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
|
|
161598
|
-
projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
|
|
161599
|
-
causes: causes(raw.causes),
|
|
161600
|
-
trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
|
|
161601
|
-
waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
|
|
161602
|
-
touchedRunIds: runIds(raw.touchedRunIds),
|
|
161603
|
-
ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
|
|
161604
|
-
groups: [],
|
|
161605
|
-
fixes: [],
|
|
161606
|
-
fixesPartial: raw.fixesPartial === true,
|
|
161607
|
-
grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
|
|
161608
|
-
};
|
|
161609
|
-
out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
|
|
161610
|
-
if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
|
|
161611
|
-
return out;
|
|
161612
|
-
}
|
|
161613
|
-
var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
|
|
161614
|
-
var init_zero_touch_view = __esm({
|
|
161615
|
-
"src/zero-touch-view.mjs"() {
|
|
161616
|
-
CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
|
|
161617
|
-
id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
|
|
161618
|
-
number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
|
|
161619
|
-
count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
|
|
161620
|
-
text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
|
|
161621
|
-
scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
|
|
161622
|
-
causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
|
|
161623
|
-
runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
|
|
161624
|
-
}
|
|
161625
|
-
});
|
|
161626
|
-
|
|
161627
|
-
// src/console/panels/zero-touch.mjs
|
|
161628
|
-
function delta(n3, previousRate = null) {
|
|
161629
|
-
if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
|
|
161630
|
-
return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
|
|
161631
|
-
}
|
|
161632
|
-
function coverage(m2) {
|
|
161633
|
-
return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
|
|
161634
|
-
}
|
|
161635
|
-
function spark(m2) {
|
|
161636
|
-
const rows3 = m2.trend;
|
|
161637
|
-
if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
|
|
161638
|
-
const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
|
|
161639
|
-
const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
|
|
161640
|
-
const y = (n3) => 56 - n3 / top * 56;
|
|
161641
|
-
const segments = [];
|
|
161642
|
-
let points = [];
|
|
161643
|
-
for (const [i2, row2] of rows3.entries()) {
|
|
161644
|
-
if (row2.rate === null || row2.partial) {
|
|
161645
|
-
if (points.length) segments.push(points);
|
|
161646
|
-
points = [];
|
|
161647
|
-
} else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
|
|
161648
|
-
}
|
|
161649
|
-
if (points.length) segments.push(points);
|
|
161650
|
-
const last = rows3[rows3.length - 1];
|
|
161651
|
-
const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
|
|
161652
|
-
return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
|
|
161653
|
-
}
|
|
161654
|
-
function longest(m2) {
|
|
161655
|
-
return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
|
|
161656
|
-
}
|
|
161657
|
-
function waitingHistory(m2) {
|
|
161658
|
-
return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
|
|
161659
|
-
}
|
|
161660
|
-
function compact(m2) {
|
|
161661
|
-
const best = longest(m2);
|
|
161662
|
-
return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
|
|
161663
|
-
}
|
|
161664
|
-
function zeroTouchRate(view) {
|
|
161665
|
-
const m2 = model5(view);
|
|
161666
|
-
return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
|
|
161667
|
-
}
|
|
161668
|
-
function zeroTouchStreak(view) {
|
|
161669
|
-
const m2 = model5(view), best = longest(m2);
|
|
161670
|
-
return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
|
|
161671
|
-
}
|
|
161672
|
-
function zeroTouchWait(view) {
|
|
161673
|
-
const m2 = model5(view);
|
|
161674
|
-
if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
|
|
161675
|
-
return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
|
|
161676
|
-
}
|
|
161677
|
-
function zeroTouchHandled(view) {
|
|
161678
|
-
const m2 = model5(view);
|
|
161679
|
-
return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
|
|
161680
|
-
}
|
|
161681
|
-
function meter3(row2) {
|
|
161682
|
-
if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
|
|
161683
|
-
const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
161684
|
-
const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
161685
|
-
return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
|
|
161686
|
-
}
|
|
161687
|
-
function rowContent(row2, sub2 = false) {
|
|
161688
|
-
const c = causesByKey.get(row2.topCause);
|
|
161689
|
-
return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
|
|
161690
|
-
}
|
|
161691
|
-
function phoneProjects(m2, view) {
|
|
161692
|
-
const item2 = (row2, repository = false) => {
|
|
161693
|
-
const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
161694
|
-
const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
161695
|
-
const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
|
|
161696
|
-
return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
|
|
161697
|
-
};
|
|
161698
|
-
return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
|
|
161699
|
-
}
|
|
161700
|
-
function zeroTouchProjects(view) {
|
|
161701
|
-
const m2 = model5(view);
|
|
161702
|
-
if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
|
|
161703
|
-
const head = html`<div class="zt-row zt-row--head" aria-hidden="true"><span></span><span>Project</span><span>Touch rate, goal tick at 5%</span><span>Against the previous interval</span><span>Needed a person</span><span>Since the last touch</span><span>Most often</span></div>`;
|
|
161704
|
-
const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
|
|
161705
|
-
return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
|
|
161706
|
-
}
|
|
161707
|
-
function causeCounts(values) {
|
|
161708
|
-
return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
|
|
161709
|
-
}
|
|
161710
|
-
function causeBar(values, max, name2) {
|
|
161711
|
-
const rows3 = causeCounts(values);
|
|
161712
|
-
let at2 = 0;
|
|
161713
|
-
max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
|
|
161714
|
-
const all = rows3.filter((r2) => r2.count > 0);
|
|
161715
|
-
const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
|
|
161716
|
-
return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
|
|
161717
|
-
const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
|
|
161718
|
-
at2 += r2.count;
|
|
161719
|
-
return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
|
|
161720
|
-
})}</svg>`;
|
|
161721
|
-
}
|
|
161722
|
-
function zeroTouchCauses(view) {
|
|
161723
|
-
const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
|
|
161724
|
-
if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
|
|
161725
|
-
const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
|
|
161726
|
-
const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
|
|
161727
|
-
return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
|
|
161728
|
-
}
|
|
161729
|
-
function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
|
|
161730
|
-
return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
|
|
161731
|
-
}
|
|
161732
|
-
function nameForm(g, index2) {
|
|
161733
|
-
const key2 = `zt-name-${index2}`;
|
|
161734
|
-
return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
|
|
161735
|
-
}
|
|
161736
|
-
function instructionForm(g, index2) {
|
|
161737
|
-
if (!g.runIds[0] || g.triggerIndex === null) return html`<p class="zt-kps">Add to instructions is unavailable: a retained run and a current non-cron trigger are required.</p>`;
|
|
161738
|
-
const key2 = `zt-sentence-${index2}`;
|
|
161739
|
-
return html`<details class="zt-editor"><summary class="link">Add to instructions</summary>${guardedForm("zero-touch-instruction", { hidden: [["runId", g.runIds[0]], ["groupId", g.id], ["index", g.triggerIndex, "int"]], fields: field2({ label: "Sentence to add", id: key2, control: textarea({ id: key2, name: "sentence", required: true, rows: 3, maxlength: 2e3 }), hint: "Preview the exact diff, then Confirm. The trigger's existing instructions count toward the 2,000-character limit." }), label: `Preview trigger ${g.triggerIndex + 1}`, kind: ["sm", "primary"] })}</details>`;
|
|
161740
|
-
}
|
|
161741
|
-
function zeroTouchQuestions(view) {
|
|
161742
|
-
if (view?.role !== "operator") return collapsed({ icon: "parked", title: "Questions it keeps asking", body: "are for operators. Rates, causes, streaks and fixes are shown here." });
|
|
161743
|
-
const m2 = model5(view);
|
|
161744
|
-
if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
|
|
161745
|
-
const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
|
|
161746
|
-
const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
|
|
161747
|
-
const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
|
|
161748
|
-
return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
|
|
161749
|
-
}
|
|
161750
|
-
function fixTone(f3) {
|
|
161751
|
-
if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
|
|
161752
|
-
return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
|
|
161753
|
-
}
|
|
161754
|
-
function fixObservation(f3) {
|
|
161755
|
-
if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
|
|
161756
|
-
if (f3.partial) return "Partial observation: effectiveness is not established.";
|
|
161757
|
-
if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
|
|
161758
|
-
return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
|
|
161759
|
-
}
|
|
161760
|
-
function beforeAfter(label3, value, max, tone = "attention") {
|
|
161761
|
-
return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
|
|
161762
|
-
}
|
|
161763
|
-
function zeroTouchFixes(view) {
|
|
161764
|
-
const m2 = model5(view);
|
|
161765
|
-
const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
|
|
161766
|
-
const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
|
|
161767
|
-
return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
|
|
161768
|
-
}
|
|
161769
|
-
var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
|
|
161770
|
-
var init_zero_touch = __esm({
|
|
161771
|
-
"src/console/panels/zero-touch.mjs"() {
|
|
161772
|
-
init_common();
|
|
161773
|
-
init_html();
|
|
161774
|
-
init_kit();
|
|
161775
|
-
init_zero_touch_view();
|
|
161776
|
-
ZERO_TOUCH_WORDS = Object.freeze({ unavailable: "Zero touch could not be read. Reload to try again; missing history is never counted as zero touches.", empty: "No workload runs ended in the covered interval.", drafts: "Sent by a person, edited or as drafted. Each run still counts as a touch.", draftsPartial: "Partial history; this counts retained runs only.", viewer: "Questions it keeps asking are for operators. Rates, causes, streaks and fixes are shown here.", noGroups: "No question groups yet. Group retained questions with a capped decision job; name each group yourself.", noFixes: "No cause-linked fixes recorded yet. Start a change from a refusal or a question group to measure it here." });
|
|
161777
|
-
ZERO_TOUCH_CAUSES = Object.freeze([
|
|
161778
|
-
{ cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
|
|
161779
|
-
{ cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
|
|
161780
|
-
{ cause: "rerun", label: "Failed or refused, then re-run", short: "Re-run", icon: "failed", tone: "failed", detail: "A person started a failed or refused run again." },
|
|
161781
|
-
{ cause: "config-fix", label: "Refused until config was fixed", short: "Config fixed", icon: "refused", tone: "refused", detail: "A confirmed configuration change explicitly targeted the refusal." },
|
|
161782
|
-
{ cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
|
|
161783
|
-
{ cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
|
|
161784
|
-
]);
|
|
161785
|
-
ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
|
|
161786
|
-
unassigned: "Not grouped yet.",
|
|
161787
|
-
pending: "A grouping job is pending.",
|
|
161788
|
-
assigned: "Assigned to a question group.",
|
|
161789
|
-
"source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
|
|
161790
|
-
"source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
|
|
161791
|
-
"source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
|
|
161792
|
-
"candidates-expired": "A group's representative context expired. Review the groups before trying again.",
|
|
161793
|
-
"group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
|
|
161794
|
-
stale: "The group revision changed during classification. Preview a fresh grouping job.",
|
|
161795
|
-
refused: "The grouping job was refused. Open its run to see the policy reason.",
|
|
161796
|
-
failed: "The grouping job failed. Open its run and review the failure before trying again."
|
|
161797
|
-
});
|
|
161798
|
-
causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
|
|
161799
|
-
model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
|
|
161800
|
-
acts3 = (v) => v?.role === "operator" && !v?.readOnly;
|
|
161801
|
-
pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
|
|
161802
|
-
amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
|
|
161803
|
-
date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
|
|
161804
|
-
span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
|
|
161805
|
-
link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
|
|
161806
|
-
fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
|
|
161807
|
-
ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
|
|
161808
|
-
ZERO_TOUCH_PANELS = Object.freeze([["p-zt-rate", zeroTouchRate], ["p-zt-streak", zeroTouchStreak], ["p-zt-wait", zeroTouchWait], ["p-zt-handled", zeroTouchHandled], ["p-zt-projects", zeroTouchProjects], ["p-zt-causes", zeroTouchCauses], ["p-zt-questions", zeroTouchQuestions], ["p-zt-fixes", zeroTouchFixes]]);
|
|
161809
|
-
}
|
|
161810
|
-
});
|
|
161811
|
-
|
|
161812
|
-
// src/trigger-policy.mjs
|
|
161813
|
-
function triggerWhoMayFire(trigger2) {
|
|
161814
|
-
const on = trigger2.on ?? trigger2;
|
|
161815
|
-
const forge = trigger2.run?.kind ?? trigger2.forge;
|
|
161816
|
-
const kind = on.type;
|
|
161817
|
-
if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
|
|
161818
|
-
if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
|
|
161819
|
-
if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
|
|
161820
|
-
if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
|
|
161821
|
-
if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
|
|
161822
|
-
if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
|
|
161823
|
-
const actions = Array.isArray(on.action) ? on.action : [];
|
|
161824
|
-
const gates = [];
|
|
161825
|
-
if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
|
|
161826
|
-
if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
|
|
161827
|
-
if (actions.includes("review_submitted")) gates.push("The reviewer's association must be OWNER, MEMBER or COLLABORATOR, regardless of the pull request author's association");
|
|
161828
|
-
if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
|
|
161829
|
-
return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
|
|
161830
|
-
}
|
|
161831
|
-
var init_trigger_policy = __esm({
|
|
161832
|
-
"src/trigger-policy.mjs"() {
|
|
161833
|
-
}
|
|
161834
|
-
});
|
|
161835
|
-
|
|
161836
161705
|
// src/trigger-gets-view.mjs
|
|
161837
161706
|
function cannotSay(row2, trigger2, fileVersion) {
|
|
161838
161707
|
if (fileVersion > 1) {
|
|
@@ -161868,17 +161737,28 @@ function agreeRow2(row2, trigger2) {
|
|
|
161868
161737
|
skills: row2.skills ?? [],
|
|
161869
161738
|
mcp: (row2.mcp ?? []).filter((server) => named2.includes(server.name) && server.enabled === true),
|
|
161870
161739
|
packages: packagesLoaded(row2, trigger2),
|
|
161871
|
-
|
|
161740
|
+
// Its answer for the file as it is, without the basis: two hosts that answer for it hold the same one, and a host on a release before
|
|
161741
|
+
// #869 holds none, which would otherwise be a difference no job meets.
|
|
161742
|
+
flows: [flowRowFor(row2, trigger2)?.row].filter(Boolean).map((found) => ({ trigger: found.trigger, flow: found.flow, tier: found.tier, package: found.package, repo: found.repo }))
|
|
161872
161743
|
};
|
|
161873
161744
|
}
|
|
161745
|
+
function flowRowFor(row2, trigger2) {
|
|
161746
|
+
if (typeof trigger2.flow !== "string") return null;
|
|
161747
|
+
const here = (row2.flows ?? []).filter((entry3) => entry3.trigger === trigger2.index);
|
|
161748
|
+
const found = here.find((entry3) => entry3.flow === trigger2.flow);
|
|
161749
|
+
if (!found) return here.some((entry3) => typeof entry3.basis === "string" && entry3.basis !== trigger2.flowBasis) ? { stale: true } : null;
|
|
161750
|
+
if (typeof found.basis === "string") return found.basis === trigger2.flowBasis ? { row: found } : { stale: true };
|
|
161751
|
+
if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
|
|
161752
|
+
if (found.tier === "package" && trigger2.packages === false) return null;
|
|
161753
|
+
return { row: found };
|
|
161754
|
+
}
|
|
161874
161755
|
function flowAnswer(row2, trigger2) {
|
|
161875
|
-
const found = (row2
|
|
161756
|
+
const found = flowRowFor(row2, trigger2)?.row;
|
|
161876
161757
|
if (!found) return null;
|
|
161877
|
-
if (found.tier === "package" && (trigger2.excludeExtensions ?? []).includes(found.package)) return null;
|
|
161878
161758
|
return found.tier === "package" ? `from ${found.package ?? "a staged package"}` : FLOW_TIER_WORDS[found.tier] ?? null;
|
|
161879
161759
|
}
|
|
161880
161760
|
function lacksFlow(row2, trigger2) {
|
|
161881
|
-
const found =
|
|
161761
|
+
const found = flowRowFor(row2, trigger2)?.row;
|
|
161882
161762
|
return found?.tier === "none" && found.repo === "absent";
|
|
161883
161763
|
}
|
|
161884
161764
|
function hostPills(rows3, trigger2, liveList, fileVersion) {
|
|
@@ -161971,8 +161851,13 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
|
|
|
161971
161851
|
const choices = offered.filter((entry3) => !seen.has(entry3.name) && seen.add(entry3.name)).slice(0, FLOW_CHOICES_MAX);
|
|
161972
161852
|
const groups = /* @__PURE__ */ new Map();
|
|
161973
161853
|
const silent = [];
|
|
161854
|
+
const stale = [];
|
|
161974
161855
|
for (const row2 of rows3) {
|
|
161975
161856
|
if (trigger2.flow === null || row2.state !== "current") continue;
|
|
161857
|
+
if (flowRowFor(row2, trigger2)?.stale === true) {
|
|
161858
|
+
stale.push(row2.name);
|
|
161859
|
+
continue;
|
|
161860
|
+
}
|
|
161976
161861
|
const word3 = flowAnswer(row2, trigger2);
|
|
161977
161862
|
if (word3 === null) silent.push(row2.name);
|
|
161978
161863
|
else groups.set(word3, [...groups.get(word3) ?? [], row2.name]);
|
|
@@ -161981,7 +161866,11 @@ function flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }) {
|
|
|
161981
161866
|
const lacking = rows3.filter((row2) => row2.state === "current" && lacksFlow(row2, trigger2)).map((row2) => row2.name);
|
|
161982
161867
|
const said = [
|
|
161983
161868
|
...[...groups].map(([word3, names2]) => `${listed(names2)} ${names2.length === 1 ? "finds" : "find"} ${trigger2.flow} ${word3}`),
|
|
161984
|
-
|
|
161869
|
+
// Issue #869: an answer judged before a change to what the flow is found by. One sentence for one host or many, so it reads alike,
|
|
161870
|
+
// and none promises a later answer: a spent one-shot, a file the loaders refuse or a host whose own triggers file differs gets none.
|
|
161871
|
+
stale.length > 0 ? `${listed(stale)} ${TRIGGER_GETS_WORDS.flowStale}` : null,
|
|
161872
|
+
// No "yet": a flow that fails the skill charset, or a spent one-shot, never gets one.
|
|
161873
|
+
silent.length > 0 ? `${listed(silent)} ${silent.length === 1 ? "has" : "have"} no answer for it` : null,
|
|
161985
161874
|
unknown3.length > 0 ? `${listed(unknown3)} ${unknown3.length === 1 ? "has" : "have"} no answer to read` : null
|
|
161986
161875
|
].filter(Boolean);
|
|
161987
161876
|
return {
|
|
@@ -162014,7 +161903,7 @@ function overlayStem(segment) {
|
|
|
162014
161903
|
for (let end = 1; end < segment.length; end += 1) if (overlayEntryNamed(segment.slice(0, end), segment)) return segment.slice(0, end);
|
|
162015
161904
|
return segment;
|
|
162016
161905
|
}
|
|
162017
|
-
function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
161906
|
+
function extensionsRow2({ trigger: trigger2, rows: rows3, last, records = [], historyUnread = false, hostLoads = null }) {
|
|
162018
161907
|
const known2 = rows3.filter((row2) => row2.state === "current");
|
|
162019
161908
|
const inFile = trigger2.excludeExtensions ?? [];
|
|
162020
161909
|
const withheld = trigger2.packages === false;
|
|
@@ -162030,10 +161919,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
162030
161919
|
for (const value of naming.length > 0 ? naming : [overlayStem(segment)]) groupOf(value).segments.add(segment);
|
|
162031
161920
|
}
|
|
162032
161921
|
for (const name2 of inFile) groupOf(name2);
|
|
162033
|
-
const
|
|
162034
|
-
|
|
162035
|
-
const
|
|
162036
|
-
|
|
161922
|
+
const said = (Array.isArray(records) ? records : []).map((record3) => triggerLastRun([record3], hostLoads).quoted).filter(Boolean);
|
|
161923
|
+
const brought = (name2, { kind, version = null }) => {
|
|
161924
|
+
for (const run3 of [last, ...said].filter(Boolean)) {
|
|
161925
|
+
const row2 = (run3.sources ?? []).find((source2) => source2.kind === kind && source2.name === name2 && (version === null || source2.version === version));
|
|
161926
|
+
if (row2) return `${broughtWords(row2) ?? "no tool, skill or prompt template"}, as ${run3.id} loaded it`;
|
|
161927
|
+
}
|
|
161928
|
+
return null;
|
|
162037
161929
|
};
|
|
162038
161930
|
const items = [...groups.values()].map(({ value, versions: versions2, segments }) => {
|
|
162039
161931
|
const on = !inFile.includes(value);
|
|
@@ -162045,12 +161937,13 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
162045
161937
|
const off = known2.filter((row2) => holdsEntry(row2) && row2.overlayExtensionsAllowed !== true).map((row2) => row2.name);
|
|
162046
161938
|
const hostsOn = (version) => known2.filter((row2) => (row2.packages ?? []).some((entry3) => entry3.name === value && entry3.version === version)).map((row2) => row2.name);
|
|
162047
161939
|
const stagedWhere = staged.length === 0 ? null : whereWords(rows3, "packages", holdsPackage, "staged", staged.length === 1 ? null : staged.map((version) => `${version} on ${listed(hostsOn(version))}`).join(", "));
|
|
161940
|
+
const fromEntries = () => entries.map((segment) => brought(segment, { kind: "overlay" })).find((words) => words !== null) ?? null;
|
|
162048
161941
|
const quoted = (() => {
|
|
162049
|
-
if (
|
|
162050
|
-
if (staged.length === 0) return
|
|
162051
|
-
const running = staged.find((version) => brought(value, version) !== null);
|
|
162052
|
-
if (running === void 0) return
|
|
162053
|
-
return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, running)}`;
|
|
161942
|
+
if (withheld && entries.length === 0) return null;
|
|
161943
|
+
if (staged.length === 0 || withheld) return fromEntries();
|
|
161944
|
+
const running = staged.find((version) => brought(value, { kind: "package", version }) !== null);
|
|
161945
|
+
if (running === void 0) return fromEntries();
|
|
161946
|
+
return `${staged.length > 1 ? `${running}: ` : ""}${brought(value, { kind: "package", version: running })}`;
|
|
162054
161947
|
})();
|
|
162055
161948
|
const also = inFile.includes(value) ? alsoNaming(inFile, value) : [];
|
|
162056
161949
|
const named2 = staged.length > 0 ? `${value} ${listed(staged)}` : entries.length > 0 && also.length === 0 ? entries.join(", ") : value;
|
|
@@ -162060,7 +161953,9 @@ function extensionsRow2({ trigger: trigger2, rows: rows3, last }) {
|
|
|
162060
161953
|
entries.length > 0 ? whereWords(rows3, "extensions", holdsEntry, "in the overlay") : null,
|
|
162061
161954
|
off.length > 0 ? `its overlay extensions off on ${listed(off)}` : null,
|
|
162062
161955
|
also.length > 0 ? `the file also names ${listed(also)}` : null,
|
|
162063
|
-
quoted
|
|
161956
|
+
quoted,
|
|
161957
|
+
// Not said where the history could not be read: the card says so once, and a failed read is not "no run".
|
|
161958
|
+
quoted === null && !historyUnread && !(withheld && entries.length === 0) && (staged.length > 0 || entries.length > 0) ? staged.length > 0 && !withheld ? TRIGGER_GETS_WORDS.bringsUnknown : TRIGGER_GETS_WORDS.overlayBringsUnknown : null
|
|
162064
161959
|
].filter(Boolean).join(" \xB7 ");
|
|
162065
161960
|
return { value, name: named2, on, moot, sub: sub2, backOn: also.length > 0 ? `${value} off the list` : `${value} back on` };
|
|
162066
161961
|
});
|
|
@@ -162163,14 +162058,69 @@ function triggerGetsOf({ trigger: trigger2, records = [], historyUnread = false,
|
|
|
162163
162058
|
last: { Model: under("Model"), Skills: under("Skills"), Extensions: under("Extensions"), MCP: under("MCP"), Tools: under("Tools"), pi: under("pi") },
|
|
162164
162059
|
model: modelRow2({ trigger: trigger2, defaults: defaults4, modelChoices }),
|
|
162165
162060
|
flow: { ...flowRow({ trigger: trigger2, rows: rows3, editor: editor3 }), ...watch ? { doctor: null, lacking: null } : {} },
|
|
162166
|
-
extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted }),
|
|
162061
|
+
extensions: extensionsRow2({ trigger: trigger2, rows: rows3, last: quoted, records, historyUnread, hostLoads }),
|
|
162167
162062
|
mcp: mcpRow2({ trigger: trigger2, rows: rows3, overlayServers }),
|
|
162168
162063
|
tools: toolsRow2({ trigger: trigger2, last: quoted }),
|
|
162169
162064
|
image: imageRow({ trigger: trigger2, rows: rows3 }),
|
|
162170
162065
|
secrets: { profile: trigger2.secretsProfile ?? null, count: trigger2.secrets ?? 0, optedOut: trigger2.secretBundle === false }
|
|
162171
162066
|
};
|
|
162172
162067
|
}
|
|
162173
|
-
|
|
162068
|
+
function freshFlowWords(row2, trigger2) {
|
|
162069
|
+
if (trigger2.flow === null) return SANDBOX_FRESH_WORDS.noFlow;
|
|
162070
|
+
const who = `a job of trigger ${trigger2.index + 1} on this host`;
|
|
162071
|
+
const at2 = flowRowFor(row2, trigger2);
|
|
162072
|
+
if (at2?.stale === true) return `Whether ${who} would find ${trigger2.flow} is unknown: ${SANDBOX_FRESH_WORDS.flowStale}.`;
|
|
162073
|
+
const word3 = flowAnswer(row2, trigger2);
|
|
162074
|
+
if (word3 === null) return `Whether ${who} would find ${trigger2.flow} is unknown: its last doctor run has no answer for it.`;
|
|
162075
|
+
const tier = at2.row.tier;
|
|
162076
|
+
if (tier === "none" || tier === "unknown") return `Whether ${who} would find ${trigger2.flow} is unknown: doctor found it ${word3}, and the repository, which is on no host, may hold it.`;
|
|
162077
|
+
if (tier === "repository") return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}.`;
|
|
162078
|
+
return `${who.charAt(0).toUpperCase()}${who.slice(1)} would find ${trigger2.flow} ${word3}, unless the repository holds it at its base commit, which comes first.`;
|
|
162079
|
+
}
|
|
162080
|
+
function freshMcpWords(row2, named2) {
|
|
162081
|
+
if (!Array.isArray(named2) || named2.length === 0) return null;
|
|
162082
|
+
const on = [];
|
|
162083
|
+
const off = [];
|
|
162084
|
+
const lacking = [];
|
|
162085
|
+
const unknown3 = [];
|
|
162086
|
+
let forge = false;
|
|
162087
|
+
for (const name2 of named2) {
|
|
162088
|
+
if (name2 === FORGE_MCP_SERVER_NAME) {
|
|
162089
|
+
forge = true;
|
|
162090
|
+
continue;
|
|
162091
|
+
}
|
|
162092
|
+
const server = (row2.mcp ?? []).find((entry3) => entry3.name === name2);
|
|
162093
|
+
if (server) (server.enabled === true ? on : off).push(name2);
|
|
162094
|
+
else (freshListsWhole(row2, "mcp") ? lacking : unknown3).push(name2);
|
|
162095
|
+
}
|
|
162096
|
+
const refused3 = [off.length > 0 ? `${listed(off)} switched off here` : null, lacking.length > 0 ? `${listed(lacking)} not on this host` : null].filter(Boolean);
|
|
162097
|
+
const said = [
|
|
162098
|
+
on.length > 0 ? `${listed(on)} on this host` : null,
|
|
162099
|
+
// The reason is not named: a host with no mcp.json publishes the same empty list, and its jobs are refused as
|
|
162100
|
+
// mcp-config-invalid rather than mcp-server-unknown (`serverRefused`). Either way, before it spends.
|
|
162101
|
+
refused3.length > 0 ? `${refused3.join(" and ")}, so a job there is refused before it spends` : null,
|
|
162102
|
+
unknown3.length > 0 ? `${listed(unknown3)} unknown here` : null,
|
|
162103
|
+
forge ? `${FORGE_MCP_SERVER_NAME}: ${TRIGGER_GETS_WORDS.forgeServer}` : null
|
|
162104
|
+
].filter(Boolean);
|
|
162105
|
+
return `${said.join("; ")}.`;
|
|
162106
|
+
}
|
|
162107
|
+
function sandboxFreshRows({ hostLoads, host, triggerIndex, flow = null, flowBasis = null, mcp = [], withholdsPackages = false, excludes = [], image = null }) {
|
|
162108
|
+
const rows3 = [[`On ${host}`, sandboxFreshLine({ hostLoads, host, triggerIndex, withholdsPackages, excludes, image, mcp })]];
|
|
162109
|
+
const row2 = hostLoadsRow(hostLoads, host);
|
|
162110
|
+
if (row2.state === "unknown") return rows3;
|
|
162111
|
+
const named2 = typeof flow === "string" && flow !== "" ? flow : null;
|
|
162112
|
+
if (row2.state !== "current") {
|
|
162113
|
+
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.`]]);
|
|
162114
|
+
const listedNames = (mcp ?? []).filter((name2) => typeof name2 === "string");
|
|
162115
|
+
if (listedNames.length > 0) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [`${listed(listedNames)} unknown here: its last doctor run is old.`]]);
|
|
162116
|
+
return rows3;
|
|
162117
|
+
}
|
|
162118
|
+
rows3.push([SANDBOX_FRESH_WORDS.flowLabel, [freshFlowWords(row2, { index: triggerIndex, flow: named2, excludeExtensions: excludes, packages: withholdsPackages ? false : void 0, flowBasis })]]);
|
|
162119
|
+
const mcpWords = freshMcpWords(row2, mcp);
|
|
162120
|
+
if (mcpWords !== null) rows3.push([SANDBOX_FRESH_WORDS.mcpLabel, [mcpWords]]);
|
|
162121
|
+
return rows3;
|
|
162122
|
+
}
|
|
162123
|
+
var TRIGGER_GETS_WORDS, code4, run2, time2, unique, overlayServersOf, packagesLoaded, overlayLoaded, serverDefinition, FLOW_TIER_WORDS, MODEL_CHOICES_MAX, PROVIDER_CHOICES_MAX, FLOW_CHOICES_MAX, listsWhole, plural7, alsoNaming, SANDBOX_FRESH_WORDS, freshListsWhole;
|
|
162174
162124
|
var init_trigger_gets_view = __esm({
|
|
162175
162125
|
"src/trigger-gets-view.mjs"() {
|
|
162176
162126
|
init_mcp_config();
|
|
@@ -162206,6 +162156,8 @@ var init_trigger_gets_view = __esm({
|
|
|
162206
162156
|
noExtensions: "The hosts that can say name no staged package or overlay extension.",
|
|
162207
162157
|
noExtensionsKnown: "No host\u2019s doctor run can say which extensions it has.",
|
|
162208
162158
|
forgeServer: "the job\u2019s own forge, with its own token; in no overlay",
|
|
162159
|
+
bringsUnknown: "what it brings is known once a run has loaded it",
|
|
162160
|
+
overlayBringsUnknown: "an overlay entry has no manifest, so what it brings is known once a run has loaded it",
|
|
162209
162161
|
noServers: "The hosts that can say name no MCP server, and this trigger names none.",
|
|
162210
162162
|
noServersKnown: "No host\u2019s doctor run can say which MCP servers it has, and this trigger names none.",
|
|
162211
162163
|
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.",
|
|
@@ -162220,6 +162172,7 @@ var init_trigger_gets_view = __esm({
|
|
|
162220
162172
|
viewer: "An operator changes these on this page, with one preview and one Confirm.",
|
|
162221
162173
|
noTool: "No tool reaches this action; dispatch_trigger_edit keeps today\u2019s scope.",
|
|
162222
162174
|
flowPicker: "It leaves out what is in a skills folder, or in a repository not on this host, and any other name can be typed.",
|
|
162175
|
+
flowStale: "answered for another version of the triggers file, so it is unknown there",
|
|
162223
162176
|
reloadDrops: "A Confirm elsewhere on this page reloads it, and drops any change not yet written in What its jobs get.",
|
|
162224
162177
|
othersUnread: "This page read the triggers file, and its other reads failed: the run history, the hosts and the jobs in line are unknown.",
|
|
162225
162178
|
pillOrder: "differs in the order or kind of what it loads",
|
|
@@ -162251,6 +162204,370 @@ var init_trigger_gets_view = __esm({
|
|
|
162251
162204
|
listsWhole = (row2, part) => row2.state === "current" && !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
|
|
162252
162205
|
plural7 = (n3, one, many = `${one}s`) => `${n3} ${n3 === 1 ? one : many}`;
|
|
162253
162206
|
alsoNaming = (list5, name2) => list5.filter((other) => other !== name2 && (overlayEntryNamed(other, name2) || overlayEntryNamed(name2, other)));
|
|
162207
|
+
SANDBOX_FRESH_WORDS = Object.freeze({
|
|
162208
|
+
noFlow: "This trigger names no flow.",
|
|
162209
|
+
flowStale: "its last doctor run answered for another version of the triggers file",
|
|
162210
|
+
flowLabel: "Flow",
|
|
162211
|
+
mcpLabel: "MCP servers"
|
|
162212
|
+
});
|
|
162213
|
+
freshListsWhole = (row2, part) => !(row2.unread ?? []).includes(part) && !((row2.more?.[part] ?? 0) > 0);
|
|
162214
|
+
}
|
|
162215
|
+
});
|
|
162216
|
+
|
|
162217
|
+
// src/console/panels/sandboxes.mjs
|
|
162218
|
+
function state2(v) {
|
|
162219
|
+
const s2 = report(v)?.request;
|
|
162220
|
+
if (!s2) return null;
|
|
162221
|
+
const success = s2.state === "ready";
|
|
162222
|
+
return banner({ status: success ? "ok" : ["refused", "failed"].includes(s2.state) ? "attention" : "running", title: success ? `Sandbox ${s2.result?.id ?? s2.id} is ready` : `Sandbox request ${s2.state}`, body: html`<span class="code">${s2.id}</span> on ${s2.host ?? "its host"}. ${s2.message ?? s2.reason ?? (success ? "No container has started. Open it from your terminal." : "Reload to read the worker's result.")}`, actions: success && s2.result ? links(v, { ...s2.result, id: s2.result.id, host: s2.result.host ?? s2.host }, "open", true) : btn({ href: `/runs/sandboxes?${new URLSearchParams({ request: s2.id, host: s2.host ?? selected3(v).host ?? "" })}`, label: "Refresh request" }) });
|
|
162223
|
+
}
|
|
162224
|
+
function sandboxFromRun(v) {
|
|
162225
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
162226
|
+
const all = rows(v);
|
|
162227
|
+
const id7 = v.query?.id;
|
|
162228
|
+
const picked = all.find((r2) => rowId2(r2) === id7 && (!v.query?.host || r2.host === v.query.host));
|
|
162229
|
+
const pickerHost = v.query?.host ?? selected3(v).host ?? all[0]?.host ?? "";
|
|
162230
|
+
const picker = html`<form method="get" action="/runs/sandboxes" data-live-hold class="sx-picker">${field2({ label: "Workspace host", id: "sb-workspace-host", control: select({ id: "sb-workspace-host", name: "host", value: pickerHost, options: [{ value: "", label: "Choose a host" }, ...(report(v)?.hosts ?? []).map((h2) => ({ value: h2.name, label: h2.name }))] }) })}${field2({ label: "Kept workspace", id: "sb-workspace", control: select({ id: "sb-workspace", name: "id", value: id7 ?? "", options: [{ value: "", label: "Choose a workspace" }, ...all.filter((r2) => !pickerHost || r2.host === pickerHost).map((r2) => ({ value: rowId2(r2), label: `${rowId2(r2)} on ${r2.host}${r2.prepared ? " (prepared)" : ""}` }))] }) })}${btn({ type: "submit", label: "Show workspace" })}</form>`;
|
|
162231
|
+
if (!report(v)) return card3("From a run", html`<p role="status">${SANDBOX_WORDS.unavailable}</p>`);
|
|
162232
|
+
if (!picked) return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<p class="card__sub">${all.length ? "Choose a kept workspace, or open Sandboxes from a run page." : SANDBOX_WORDS.empty}</p>`, "the image, workspace and isolation the run kept");
|
|
162233
|
+
const workspace = picked.kind === "local" ? "Its original host folder" : isForgeKind(picked.kind) ? "Its retained clone" : "The workspace recorded in its kept manifest";
|
|
162234
|
+
const piVersion = hostPiFor(hostLoadsRow(v.data?.hostLoads, picked.host), picked.image);
|
|
162235
|
+
const loads = picked.prepared ? sandboxPreparedLine(piVersion) : sandboxRunLine(v.data?.sandboxRun ?? null, rowId2(picked), { pi: piVersion, unread: v.data?.sandboxRunUnread === true });
|
|
162236
|
+
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 });
|
|
162237
|
+
return card3("From a run", html`${state2(v)}${v.data?.sandboxHostsProblem ? html`<p role="status">${v.data.sandboxHostsProblem}</p>` : null}${picker}<div class="sx-run">${tile("ok", { icon: "box" })}<div><p class="sx-run__t"><span class="code">${rowId2(picked)}</span>${picked.project ? ptag(picked.project) : null}${picked.prepared ? chip("Without a run") : null}</p><p class="sx-run__s">${picked.host} ${vtag(picked.v ?? picked.backend)}. ${retention(picked, v.now)}</p></div></div><h3 class="sx-h3">What opens</h3>${what2}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.sandboxFoot}</span></p><h3 class="sx-h3">How it opens</h3>${canOpen(picked) ? html`<div class="sx-ch sx-ch--on"><div class="sx-ch__h">${icon("terminal", { size: "sm" })}<b>In your terminal</b><span class="sx-rec sandbox-opener">recommended</span></div><p class="sx-ch__s">Your terminal opens with the command typed in and waits for Enter. The link holds no secret. On a phone, copy the command.</p>${links(v, picked)}</div><div class="sx-foot"><p>${icon("info", { size: "sm" })}<span>The shell uses a job's isolation and network. A published host port is refused while egress is on. Pinning uses this host's configured pin window.</span></p>${pinForm(v, picked)}</div>` : html`<p>This venue has no sandbox shell. Its workspace cannot be reopened here.</p>`}`, "the same image, workspace and isolation as the run");
|
|
162238
|
+
}
|
|
162239
|
+
function sandboxNew(v) {
|
|
162240
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
162241
|
+
const s2 = selected3(v), data5 = report(v), options = data5?.options ?? [];
|
|
162242
|
+
const repositories = [...new Set(options.map((o) => o.repo).filter(Boolean))];
|
|
162243
|
+
const triggers2 = options;
|
|
162244
|
+
const hostOptions = (data5?.hosts ?? []).filter((h2) => !h2.unreadable && !h2.stale && h2.settings && (!data5?.eligibleHosts || data5.eligibleHosts.includes(h2.name))).map((h2) => ({ value: h2.name, label: `${h2.name}, ${h2.settings.defaultBackend ?? "venue not published"}` }));
|
|
162245
|
+
const fields3 = [
|
|
162246
|
+
field2({ label: "Repository", id: "sb-repo", control: input({ id: "sb-repo", name: "repo", value: s2.repo ?? "", placeholder: "owner/repository", required: true, autocomplete: "off", spellcheck: false }) }),
|
|
162247
|
+
field2({ label: "As the jobs of", id: "sb-trigger", control: select({ id: "sb-trigger", name: "index", value: s2.index === void 0 ? "" : String(s2.index), options: [{ value: "", label: "Choose a trigger" }, ...triggers2.map((o) => ({ value: String(o.index), label: o.label }))] }), hint: "Its image, size and egress are shown below, rather than chosen here." }),
|
|
162248
|
+
field2({ label: "Start from", id: "sb-ref", control: input({ id: "sb-ref", name: "ref", value: s2.ref ?? "HEAD", code: true, required: true, autocomplete: "off", spellcheck: false }), hint: "HEAD means the default branch. Enter a branch, tag, full commit SHA, or pr:42 for a pull request." }),
|
|
162249
|
+
field2({ label: "Host", id: "sb-host", control: select({ id: "sb-host", name: "host", value: s2.host ?? "", options: [{ value: "", label: "Choose a host" }, ...hostOptions] }), hint: "The worker checks the trigger and its own policy again before cloning." }),
|
|
162250
|
+
field2({ label: "Keep it", id: "sb-pin", control: select({ id: "sb-pin", name: "pin", value: s2.pin ? "1" : "0", options: [{ value: "0", label: "Host's retention window" }, { value: "1", label: "Host's pin window" }] }) })
|
|
162251
|
+
];
|
|
162252
|
+
const plan = data5?.plan;
|
|
162253
|
+
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;
|
|
162254
|
+
const form = html`${source2}<form method="get" action="/runs/sandboxes" data-live-hold>${data5?.fromRun ? html`<input type="hidden" name="fromRun" value="${data5.fromRun}">` : null}<div class="sx-form">${fields3}</div><div class="sx-form-end">${btn({ type: "submit", label: "Review settings" })}</div></form>`;
|
|
162255
|
+
const fresh = plan ? html`${facts(sandboxFreshRows({ hostLoads: v.data?.hostLoads, host: plan.host, triggerIndex: s2.index, flow: data5?.flow ?? null, flowBasis: data5?.flowBasis ?? null, mcp: data5?.mcp ?? [], withholdsPackages: data5?.withholdsPackages === true, excludes: data5?.excludes ?? [], image: plan.image }).map(([label3, parts2]) => [label3, wordsHtml(parts2)]), { cols: 1 })}<p class="sx-small">${icon("info", { size: "sm" })}<span>${LOADED_WORDS.freshFoot}</span></p>` : null;
|
|
162256
|
+
const preparation = plan ? html`<h3 class="sx-h3">Given by the trigger, not chosen here</h3>${facts5(plan)}${fresh}<p class="sx-p">${plan.pin ? `Pinned for ${plan.pinDays} days` : `Kept for ${plan.retentionHours} hours`}, on ${plan.host}. The worker clones on that host with a credential for this repository, then drops it. No container starts, no model runs, no dollars are spent and no run slot is taken.</p>${acts2(v) ? actForm({ action: "sandbox-prepare", hidden: [["repo", s2.repo], ["index", s2.index, "int"], ["ref", s2.ref], ["host", s2.host], ["pin", Boolean(s2.pin), "json"]], label: "Preview preparation", kind: ["primary"], icon: "plus" }) : html`<p class="card__sub">This console is read-only. Prepare from the host's terminal.</p>`}${cmd(["pi-dispatch", "sandbox", "--new", s2.repo, "--ref", s2.ref, "--as", String(s2.index), ...s2.pin ? ["--pin"] : []])}<p class="card__sub">The CLI’s --as takes a zero-based file index: trigger row ${s2.index + 1} is index ${s2.index}.</p>` : html`<p class="sx-p" role="status">${data5?.refusal ?? "Choose a repository, its trigger, a ref and a host, then review their settings."}</p>`;
|
|
162257
|
+
return card3("Without a run", html`${form}${preparation}<noscript><p class="card__sub">${SANDBOX_WORDS.noScript}</p></noscript>`, "a fresh clone with a trigger's image and a job's isolation");
|
|
162258
|
+
}
|
|
162259
|
+
function sandboxOpen(v) {
|
|
162260
|
+
if (!allowed3(v)) return null;
|
|
162261
|
+
const data5 = report(v), sessions = (data5?.sessions ?? []).filter((s2) => !v.query?.gatewaySelect || s2.host === v.query.host);
|
|
162262
|
+
const incomplete = !data5 || data5.sessionsUnavailable || data5.sessionsMore > 0 || data5.sessionsUnreadable > 0;
|
|
162263
|
+
const notices = (data5?.notices ?? []).filter((n3) => !v.query?.gatewaySelect || n3.host === v.query.host).map((n3) => html`<p class="sx-p" role="status">${icon("attention", { size: "sm" })} ${n3.host}, ${n3.id}: ${{ "network-kept": "The sandbox network was kept because cleanup could not be confirmed. Inspect this host\u2019s runtime and network before removing either.", "network-cleanup-failed": "Sandbox network cleanup failed. Inspect this host\u2019s runtime and egress network before retrying cleanup.", "runtime-unreadable": "The runtime could not be read. An expired shell registration does not prove the container stopped. Inspect this host before reopening or cleaning it." }[n3.reason] ?? "Sandbox cleanup needs inspection on this host."} <a class="link" href="${`/fleet/host?${new URLSearchParams({ name: n3.host })}`}">Inspect host</a>.</p>`);
|
|
162264
|
+
if (v.query?.gatewaySelect && !v.data?.browserGateways?.enabledHosts?.includes(v.query.host)) return card3("Open now", html`<p role="status">The browser gateway is not enabled for ${v.query.host}, so there is no shell to choose here. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>`);
|
|
162265
|
+
return card3("Open now", html`${v.query?.gatewaySelect ? html`<p class="sx-p">Choose an already-open shell on ${v.query.host}. Each browser opening needs your current operator token. <a class="link" href="/runs/sandboxes">Return to Sandboxes</a>.</p>` : null}${notices}${data5?.noticesUnavailable ? html`<p role="status">Some sandbox cleanup notices could not be read. Inspect the host if a terminal ended unexpectedly.</p>` : null}${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? html`<p role="status">The registered shell list is incomplete: at least ${data5.sessionsMore ?? 0} more and ${data5.sessionsUnreadable ?? 0} unreadable registrations.</p>` : null}${data5?.sessionsUnavailable ? html`<p role="status">Open shells could not be read. Reload to try again.</p>` : sessions.length ? html`<ul class="sx-opens">${sessions.map((s2) => html`<li class="sx-open"><span class="dot tone-running"></span><div class="sx-open__b"><p class="sx-open__t"><span class="code">${s2.id}</span>${vtag(s2.venue)}</p><p class="sx-open__s">${s2.host}, opened by ${s2.by ?? "the terminal operator"}. Open since ${stamp(s2.since)}.</p>${sandboxDriverLine(v, s2)}<div class="sx-open__a">${links(v, s2, "attach", true)}${sandboxBrowserLink(v, s2)}${acts2(v) && !v.query?.gatewaySelect ? actForm({ action: "sandbox-end", hidden: [["host", s2.host], ["id", s2.id], ["nonce", s2.nonce], ["containerId", s2.containerId]], label: "Preview End", kind: ["sm", "danger"], icon: "cancel" }) : null}</div></div></li>`)}</ul>` : html`<p>${data5?.sessionsMore > 0 || data5?.sessionsUnreadable > 0 ? "No readable shell is in this bounded view. The incomplete registrations above prevent an exact empty count." : SANDBOX_WORDS.noShells}</p>`}<p class="sx-small">${icon("info", { size: "sm" })}<span>One shell per workspace at a time. End asks the terminal that opened it to close the shell and keeps the workspace. What you type is not recorded.</span></p>`, null, badge(incomplete ? "attention" : "running", `${sessions.length} ${incomplete ? "shown" : "open"}`));
|
|
162266
|
+
}
|
|
162267
|
+
function sandboxKept(v) {
|
|
162268
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
162269
|
+
const data5 = report(v), all = rows(v);
|
|
162270
|
+
return card3("Kept workspaces", html`${data5?.more > 0 ? html`<p role="status">At least ${data5.more} more hosts have inventory beyond this bounded view. Inspect the fleet before treating this list as complete.</p>` : null}${!data5 ? html`<p role="status">${SANDBOX_WORDS.unavailable}</p>` : html`${(data5.hosts ?? []).filter((h2) => h2.stale || h2.unreadable).map((h2) => html`<p class="sx-p" role="status">${h2.name}: ${h2.unreadable ? "publication unreadable" : "publication stale; inspect this host before opening"}. Its list is incomplete.</p>`)}${all.length ? html`<ul class="sx-kepts">${all.map((r2) => html`<li class="sx-kept${canOpen(r2) ? "" : " sx-kept--no"}"><p class="sx-kept__t"><a class="code" href="${`/runs/sandboxes?${new URLSearchParams({ id: rowId2(r2), host: r2.host })}`}">${rowId2(r2)}</a>${r2.project ? ptag(r2.project) : null}${r2.prepared ? chip("Without a run") : null}</p><p class="sx-kept__s">${r2.host} ${vtag(r2.v ?? r2.backend)}. ${retention(r2, v.now)}${!canOpen(r2) ? ". This venue has no sandbox shell." : null}</p>${canOpen(r2) ? html`<div class="sx-kept__actions">${links(v, r2, "open", true)}${pinForm(v, r2)}</div>` : null}</li>`)}</ul>` : html`<p>${SANDBOX_WORDS.empty}</p>`}${(data5.hosts ?? []).filter((h2) => h2.more > 0).map((h2) => html`<p class="sx-p">${h2.name} has at least ${h2.more} more retained workspaces. Inspect its full list with pi-dispatch sandbox --list.</p>`)}`}`, "what each host still keeps");
|
|
162271
|
+
}
|
|
162272
|
+
function sandboxBrowser(v) {
|
|
162273
|
+
if (!allowed3(v) || v.query?.gatewaySelect) return null;
|
|
162274
|
+
const gateway = v.data?.browserGateways;
|
|
162275
|
+
const count7 = gateway?.enabledHosts?.length ?? 0;
|
|
162276
|
+
return card3("In this browser", html`<p class="sx-p">${SANDBOX_WORDS.browser}</p>${gateway?.problem ? html`<p class="sx-p" role="status">${gateway.problem}</p>` : html`<p class="sx-p">${count7 ? `${count7} ${count7 === 1 ? "host is" : "hosts are"} enrolled.` : "No host is enrolled in this console. Ask the host operator to configure the optional gateway."}${count7 ? html`<span class="sandbox-opener"> Choose Open in browser beside a registered shell in Open now.</span>` : null}</p>`}<p class="sx-p">Each opening needs your current operator token again. Only one browser window attaches at a time. Closing it keeps the original sandbox; what you type is not recorded.</p><p class="sx-p">First open a kept or prepared workspace in your own terminal. The browser never starts a container.</p><p class="sx-p">Browser terminals require a window wider than 760 pixels. On a phone, copy the command for your own terminal.</p>${v.readOnly ? html`<p class="sx-p">This console is read-only, so browser attachment is unavailable.</p>` : null}`, "optional, separate gateway", chip(count7 ? "Enabled per host" : "Off by default", { status: count7 ? "ok" : "idle", icon: "shield" }));
|
|
162277
|
+
}
|
|
162278
|
+
var SANDBOX_WORDS, report, allowed3, acts2, rows, card3, stamp, rowId2, canOpen, selected3, facts5, links, retention, pinForm, SANDBOX_PANELS;
|
|
162279
|
+
var init_sandboxes = __esm({
|
|
162280
|
+
"src/console/panels/sandboxes.mjs"() {
|
|
162281
|
+
init_html();
|
|
162282
|
+
init_kit();
|
|
162283
|
+
init_forges();
|
|
162284
|
+
init_sandbox_links();
|
|
162285
|
+
init_common();
|
|
162286
|
+
init_loaded_words();
|
|
162287
|
+
init_loaded_parts();
|
|
162288
|
+
init_trigger_gets_view();
|
|
162289
|
+
SANDBOX_WORDS = Object.freeze({
|
|
162290
|
+
unavailable: "Sandbox information could not be read. Reload to try again; an unread host is not an empty host.",
|
|
162291
|
+
empty: "No kept workspace was published. A worker publishes its retained workspaces while it is running.",
|
|
162292
|
+
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.",
|
|
162293
|
+
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.",
|
|
162294
|
+
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."
|
|
162295
|
+
});
|
|
162296
|
+
report = (v) => v.data?.sandboxes;
|
|
162297
|
+
allowed3 = (v) => v.role === "operator";
|
|
162298
|
+
acts2 = (v) => allowed3(v) && !v.readOnly;
|
|
162299
|
+
rows = (v) => (report(v)?.hosts ?? []).flatMap((h2) => (h2.rows ?? []).map((r2) => ({ ...r2, host: h2.name, hostStale: h2.stale, retentionHours: h2.retentionHours })));
|
|
162300
|
+
card3 = (title2, body, sub2 = null, end = null) => card({ title: title2, sub: sub2, body, end });
|
|
162301
|
+
stamp = (ms3) => Number.isFinite(ms3) ? timeHtml(ms3) : "not published";
|
|
162302
|
+
rowId2 = (r2) => r2.id ?? r2.jobId;
|
|
162303
|
+
canOpen = (r2) => !String(rowId2(r2)).startsWith("sha256:") && r2.canOpen !== false && ["local", "podman"].includes(r2.v ?? r2.backend);
|
|
162304
|
+
selected3 = (v) => report(v)?.selected ?? {};
|
|
162305
|
+
facts5 = (plan) => facts([
|
|
162306
|
+
["Image", plan.image ?? "not published"],
|
|
162307
|
+
["Size", plan.size ? `${plan.size.memMiB} MiB, ${plan.size.cpuCenti / 100} CPU` : "not published"],
|
|
162308
|
+
["Network", plan.egress?.armed ? "The host's job allowlist through its egress proxy" : "Open bridge, egress policy off"],
|
|
162309
|
+
["Credentials", "None in the shell"]
|
|
162310
|
+
]);
|
|
162311
|
+
links = (v, r2, verb = "open", compact2 = false) => sandboxLinks({ id: rowId2(r2), host: r2.host, verb, compact: compact2, config: v.data?.sandboxHosts });
|
|
162312
|
+
retention = (r2, now) => html`${r2.pin ? "Pinned" : "Kept"}${Number.isFinite(r2.until) ? html` until ${stamp(r2.until)}${r2.until <= now ? " (past its window; pin before opening)" : html`, ${ageText(r2.until - now)} left`}` : ": deadline not published"}`;
|
|
162313
|
+
pinForm = (v, r2) => acts2(v) && canOpen(r2) ? actForm({ action: "sandbox-pin", hidden: [["host", r2.host], ["id", rowId2(r2)]], label: "Preview pin", icon: "clock" }) : null;
|
|
162314
|
+
SANDBOX_PANELS = Object.freeze([["p-sb-run", sandboxFromRun], ["p-sb-new", sandboxNew], ["p-sb-open", sandboxOpen], ["p-sb-kept", sandboxKept], ["p-sb-browser", sandboxBrowser]]);
|
|
162315
|
+
}
|
|
162316
|
+
});
|
|
162317
|
+
|
|
162318
|
+
// src/zero-touch-view.mjs
|
|
162319
|
+
function breakdown(row2, repository = false) {
|
|
162320
|
+
const out = { project: text3(row2?.project ?? row2?.id, 64), scopeId: scope(row2?.scopeId), name: text3(row2?.name ?? row2?.target ?? row2?.project ?? row2?.id) ?? "Unassigned", target: text3(row2?.target), runs: count5(row2?.runs), touched: count5(row2?.touched), rate: number3(row2?.rate), deltaPoints: Number.isFinite(row2?.deltaPoints) ? row2.deltaPoints : null, previousRate: number3(row2?.previousRate), streakDays: row2?.waitingPartial === true ? null : number3(row2?.streakDays), zeroTouch: row2?.zeroTouch === true && row2?.waitingPartial !== true, waitingNow: row2?.waitingPartial === true ? null : count5(row2?.waitingNow ?? row2?.waiting), waitingPartial: row2?.waitingPartial === true, topCause: CAUSES.includes(row2?.topCause) ? row2.topCause : null, causes: causes(row2?.causes), touchedRunIds: runIds(row2?.touchedRunIds) };
|
|
162321
|
+
if (!repository) out.repositories = Array.isArray(row2?.repositories) ? row2.repositories.map((r2) => breakdown(r2, true)) : [];
|
|
162322
|
+
return out;
|
|
162323
|
+
}
|
|
162324
|
+
function zeroTouchView(raw, { role = "viewer", windowDays = 30, nowMs: nowMs2 = 0 } = {}) {
|
|
162325
|
+
const observed = raw?.coverage ?? raw?.window;
|
|
162326
|
+
const unobserved2 = Array.isArray(observed?.reasons) && observed.reasons.includes("touch-index-unavailable") || observed?.partial === true && number3(observed.fromMs) !== null && observed.fromMs >= (number3(raw?.nowMs) ?? nowMs2);
|
|
162327
|
+
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: [] } };
|
|
162328
|
+
const totals = raw.totals ?? raw.summary ?? {};
|
|
162329
|
+
const coverage2 = raw.coverage ?? raw.window ?? {};
|
|
162330
|
+
const out = {
|
|
162331
|
+
state: "ready",
|
|
162332
|
+
nowMs: number3(raw.nowMs) ?? nowMs2,
|
|
162333
|
+
windowDays: [7, 30, 90].includes(raw.windowDays ?? raw.days) ? raw.windowDays ?? raw.days : windowDays,
|
|
162334
|
+
coverage: { requestedFromMs: number3(coverage2.requestedFromMs), fromMs: number3(coverage2.fromMs), partial: coverage2.partial === true || coverage2.complete === false, reasons: Array.isArray(coverage2.reasons) ? coverage2.reasons.map((v) => text3(v)).filter(Boolean) : [] },
|
|
162335
|
+
totals: { runs: count5(totals.runs), touched: count5(totals.touched), rate: number3(totals.rate), previousRate: number3(totals.previousRate), stopped: count5(totals.stopped), stoppedUnattributed: count5(totals.stoppedUnattributed), waitingHours: totals.waitingPartial === true ? null : number3(totals.waitingHours), waitingPartial: totals.waitingPartial === true, waitingUnknown: count5(totals.waitingUnknown), medianWaitMs: totals.waitingPartial === true ? null : number3(totals.medianWaitMs) ?? (number3(totals.medianWaitHours) === null ? null : totals.medianWaitHours * 36e5), pending: totals.waitingPartial === true ? null : count5(totals.pending ?? totals.waitingNow), draftAnswers: count5(totals.draftAnswers), deltaPoints: Number.isFinite(totals.deltaPoints) ? totals.deltaPoints : null },
|
|
162336
|
+
projects: Array.isArray(raw.projects) ? raw.projects.map((r2) => breakdown(r2)) : [],
|
|
162337
|
+
causes: causes(raw.causes),
|
|
162338
|
+
trend: Array.isArray(raw.trend) ? raw.trend.slice(-8).map((r2) => ({ atMs: number3(r2?.atMs ?? r2?.at), rate: number3(r2?.rate), runs: count5(r2?.runs), touched: count5(r2?.touched), partial: r2?.partial === true })) : [],
|
|
162339
|
+
waitingRuns: Array.isArray(raw.waitingRuns ?? totals.waitingNow) ? (raw.waitingRuns ?? totals.waitingNow).map((r2) => ({ runId: id5(r2?.runId ?? r2?.jobId), parkedAt: number3(r2?.parkedAt) })).filter((r2) => r2.runId && r2.parkedAt !== null) : [],
|
|
162340
|
+
touchedRunIds: runIds(raw.touchedRunIds),
|
|
162341
|
+
ungroupedRunIds: role === "operator" ? runIds(raw.ungroupedRunIds) : [],
|
|
162342
|
+
groups: [],
|
|
162343
|
+
fixes: [],
|
|
162344
|
+
fixesPartial: raw.fixesPartial === true,
|
|
162345
|
+
grouping: { enabled: raw.grouping?.enabled === true, partial: raw.grouping?.partial === true, pending: count5(raw.grouping?.pending), unassigned: count5(raw.grouping?.unassigned), state: ["ready", "ok", "disabled", "pending", "partial", "failed", "configuration-unavailable", "unavailable"].includes(raw.grouping?.state) ? raw.grouping.state : "disabled", sources: role === "operator" && Array.isArray(raw.grouping?.sources ?? raw.groupingStatuses) ? (raw.grouping?.sources ?? raw.groupingStatuses).map((r2) => ({ runId: id5(r2?.runId), status: ["unassigned", "pending", "assigned", "source-expired", "source-unavailable", "source-oversize", "candidates-expired", "group-limit", "stale", "refused", "failed"].includes(r2?.status) ? r2.status : "source-unavailable" })).filter((r2) => r2.runId) : [] }
|
|
162346
|
+
};
|
|
162347
|
+
out.fixes = Array.isArray(raw.fixes) ? raw.fixes.map((f3) => ({ id: id5(f3?.id), at: number3(f3?.at), undoneAt: number3(f3?.undoneAt), kind: ["instructions", "config"].includes(f3?.kind) ? f3.kind : "config", groupId: id5(f3?.groupId), reason: typeof f3?.reason === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(f3.reason) ? f3.reason : null, triggerIndex: Number.isSafeInteger(f3?.triggerIndex) && f3.triggerIndex >= 0 ? f3.triggerIndex : null, beforeCount: number3(f3?.beforeCount), afterCount: number3(f3?.afterCount), spanMs: number3(f3?.spanMs), partial: f3?.partial !== false, subsequentRuns: count5(f3?.subsequentRuns), status: ["observing", "improved", "unchanged", "worse"].includes(f3?.status) ? f3.status : "observing" })).filter((f3) => f3.id) : [];
|
|
162348
|
+
if (role === "operator") out.groups = Array.isArray(raw.groups) ? raw.groups.map((g) => ({ id: id5(g?.id), name: text3(g?.name, 120), scopeId: scope(g?.scopeId), repo: text3(g?.repo), project: text3(g?.project, 64), count: count5(g?.count), runIds: runIds(g?.runIds), status: ["open", "fixed", "unassigned"].includes(g?.status) ? g.status : "open", fixedAt: number3(g?.fixedAt), triggerIndex: Number.isSafeInteger(g?.triggerIndex) && g.triggerIndex >= 0 ? g.triggerIndex : null })).filter((g) => g.id) : [];
|
|
162349
|
+
return out;
|
|
162350
|
+
}
|
|
162351
|
+
var CAUSES, id5, number3, count5, text3, scope, causes, runIds;
|
|
162352
|
+
var init_zero_touch_view = __esm({
|
|
162353
|
+
"src/zero-touch-view.mjs"() {
|
|
162354
|
+
CAUSES = Object.freeze(["question-answer", "approval-decision", "rerun", "config-fix", "steer", "cancel"]);
|
|
162355
|
+
id5 = (v) => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : null;
|
|
162356
|
+
number3 = (v) => Number.isFinite(v) && v >= 0 ? v : null;
|
|
162357
|
+
count5 = (v) => Number.isSafeInteger(v) && v >= 0 ? v : 0;
|
|
162358
|
+
text3 = (v, max = 200) => typeof v === "string" && [...v].length <= max && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(v) ? v : null;
|
|
162359
|
+
scope = (v) => typeof v === "string" && /^[a-f0-9]{32,64}$/.test(v) ? v : null;
|
|
162360
|
+
causes = (v) => CAUSES.map((cause) => ({ cause, count: count5(Array.isArray(v) ? v.find((row2) => row2?.cause === cause)?.count : v?.[cause]) }));
|
|
162361
|
+
runIds = (v) => Array.isArray(v) ? [...new Set(v.map(id5).filter(Boolean))].slice(0, 5e3) : [];
|
|
162362
|
+
}
|
|
162363
|
+
});
|
|
162364
|
+
|
|
162365
|
+
// src/console/panels/zero-touch.mjs
|
|
162366
|
+
function delta(n3, previousRate = null) {
|
|
162367
|
+
if (!Number.isFinite(n3)) return html`<span class="zt-tr">Previous interval not covered</span>`;
|
|
162368
|
+
return html`<span class="${`zt-tr${n3 < 0 ? " tone-ok" : n3 > 0 ? " tone-attention" : ""}`}">${icon(n3 < 0 ? "chev" : n3 > 0 ? "chev-r" : "info", { size: "sm" })}<b>${n3 === 0 ? "no change" : `${n3 < 0 ? "down" : "up"} ${amount3(Math.abs(n3))} points`}</b>${previousRate === null ? null : html`<small>from ${pct2(previousRate)}</small>`}</span>`;
|
|
162369
|
+
}
|
|
162370
|
+
function coverage(m2) {
|
|
162371
|
+
return html`<p class="zt-coverage" role="status">${m2.coverage.partial ? html`<b>Partial ${m2.windowDays}-day window.</b> Covered from ${date(m2.coverage.fromMs)} UTC. Older unknown runs are not counted as untouched.` : html`Whole ${m2.windowDays}-day window.`}${m2.coverage.reasons.length ? html` ${m2.coverage.reasons.join("; ")}.` : null}</p>`;
|
|
162372
|
+
}
|
|
162373
|
+
function spark(m2) {
|
|
162374
|
+
const rows3 = m2.trend;
|
|
162375
|
+
if (!rows3.some((r2) => Number.isFinite(r2.rate) && !r2.partial)) return html`<p class="zt-kps">Weekly trend is not covered yet.</p>`;
|
|
162376
|
+
const top = Math.max(32, ...rows3.map((r2) => r2.rate ?? 0));
|
|
162377
|
+
const x2 = (i2) => rows3.length === 1 ? 200 : i2 * 200 / (rows3.length - 1);
|
|
162378
|
+
const y = (n3) => 56 - n3 / top * 56;
|
|
162379
|
+
const segments = [];
|
|
162380
|
+
let points = [];
|
|
162381
|
+
for (const [i2, row2] of rows3.entries()) {
|
|
162382
|
+
if (row2.rate === null || row2.partial) {
|
|
162383
|
+
if (points.length) segments.push(points);
|
|
162384
|
+
points = [];
|
|
162385
|
+
} else points.push(`${svgNum(x2(i2), 0, 200)},${svgNum(y(row2.rate), 0, 56)}`);
|
|
162386
|
+
}
|
|
162387
|
+
if (points.length) segments.push(points);
|
|
162388
|
+
const last = rows3[rows3.length - 1];
|
|
162389
|
+
const label3 = `Rolling ${m2.windowDays}-day touch rate at weekly intervals: ${rows3.map((r2) => r2.partial || r2.rate === null ? "not covered" : pct2(r2.rate)).join(", ")}. Goal under 5 percent.`;
|
|
162390
|
+
return html`<svg class="zt-spark" viewBox="-4 -4 208 64" preserveAspectRatio="none" role="img" aria-label="${label3}"><line class="zt-spark__goal" x1="0" x2="200" y1="${svgNum(y(5), 0, 56)}" y2="${svgNum(y(5), 0, 56)}"/>${segments.map((p) => html`<polyline class="zt-spark__l" points="${p.join(" ")}"/>`)}${last?.rate !== null && !last?.partial ? html`<circle class="zt-spark__d" cx="200" cy="${svgNum(y(last.rate), 0, 56)}" r="3.5"/>` : null}</svg><p class="zt-spark__ax"><span>${date(rows3[0]?.atMs)}</span><span>goal, dashed</span><span>now</span></p><p class="sr">${label3}</p>`;
|
|
162391
|
+
}
|
|
162392
|
+
function longest(m2) {
|
|
162393
|
+
return [...m2.projects].filter((p) => p.streakDays !== null && !p.waitingNow).sort((a, b) => b.streakDays - a.streakDays)[0] ?? null;
|
|
162394
|
+
}
|
|
162395
|
+
function waitingHistory(m2) {
|
|
162396
|
+
return m2.totals.waitingPartial ? html`<p class="zt-note" role="status">Waiting history is incomplete. An older park predates retained answer history; its current state and wait time are not known.</p>` : null;
|
|
162397
|
+
}
|
|
162398
|
+
function compact(m2) {
|
|
162399
|
+
const best = longest(m2);
|
|
162400
|
+
return html`<ul class="ztp-now"><li><span>${icon("flag", { size: "sm" })}Longest streak</span><b>${best ? `${best.name}, ${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history"}</b></li><li><span>${icon("clock", { size: "sm" })}Waited on a person</span><b>${m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h, median ${span(m2.totals.medianWaitMs)}`}</b></li><li><span>${icon("edit", { size: "sm" })}Answered from a draft</span><b>${m2.totals.draftAnswers} ${m2.totals.draftAnswers === 1 ? "run" : "runs"}</b><span>${ZERO_TOUCH_WORDS.drafts}${m2.coverage.partial ? html` ${ZERO_TOUCH_WORDS.draftsPartial}` : null}</span></li><li><span>${icon("parked", { size: "sm" })}Waiting now</span><b>${m2.totals.waitingPartial ? "Not known" : `${m2.totals.pending} ${m2.totals.pending === 1 ? "run" : "runs"}`}</b>${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</li></ul>${waitingHistory(m2)}`;
|
|
162401
|
+
}
|
|
162402
|
+
function zeroTouchRate(view) {
|
|
162403
|
+
const m2 = model5(view);
|
|
162404
|
+
return card({ title: `Touch rate, ${m2.windowDays} days`, body: html`${fail5(m2)}${m2.state === "ready" ? html`${kpi(m2.totals.runs ? pct2(m2.totals.rate) : "No runs", `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`)}${spark(m2)}<p class="zt-kp"><span class="zt-kt">${icon("flag", { size: "sm" })}goal under 5%</span>${delta(m2.totals.deltaPoints, m2.totals.previousRate)}</p>${coverage(m2)}<p class="zt-kps">${m2.totals.stopped} stopped before they ran, outside the rate.${m2.totals.stoppedUnattributed > 0 ? html` ${m2.totals.stoppedUnattributed} stops have no recorded project or scope, so this selection may have more.` : null}</p><div class="zt-phone-summary">${compact(m2)}</div>` : null}` });
|
|
162405
|
+
}
|
|
162406
|
+
function zeroTouchStreak(view) {
|
|
162407
|
+
const m2 = model5(view), best = longest(m2);
|
|
162408
|
+
return card({ title: "Zero-touch streak", body: html`${fail5(m2)}${kpi(best ? `${amount3(best.streakDays)} ${best.streakDays === 1 ? "day" : "days"}` : "Not enough history", best ? `${best.name}, the longest now` : "30 days of observed activity establish a zero-touch project")}<p class="zt-kp">${m2.projects.filter((p) => p !== best).sort((a, b) => (b.streakDays ?? -1) - (a.streakDays ?? -1)).map((p) => p.waitingNow ? chip(`${p.name} waiting now`, { status: "parked", icon: "parked" }) : html`<span class="zt-kt">${p.name} ${p.streakDays === null ? "not known" : `${amount3(p.streakDays)} ${p.streakDays === 1 ? "day" : "days"}`}</span>`)}</p>` });
|
|
162409
|
+
}
|
|
162410
|
+
function zeroTouchWait(view) {
|
|
162411
|
+
const m2 = model5(view);
|
|
162412
|
+
if (m2.state !== "ready") return card({ title: "Waiting for a person", body: fail5(m2) });
|
|
162413
|
+
return card({ title: "Waiting for a person", body: html`${fail5(m2)}${kpi(m2.totals.waitingPartial ? "Not known" : `${amount3(m2.totals.waitingHours)}h`, `runs waited on a person in ${m2.windowDays} days`)}<p class="zt-kps">Median wait for an answer <b>${span(m2.totals.medianWaitMs)}</b></p>${waitingHistory(m2)}<p class="zt-kp">${chip(m2.totals.waitingPartial ? "Pending count not known" : `${m2.totals.pending} waiting now`, { status: "parked", icon: "parked" })}${m2.waitingRuns.slice(0, 3).map((r2) => html`<span>${link(r2.runId)}, ${span(m2.nowMs - r2.parkedAt)}</span>`)}</p>` });
|
|
162414
|
+
}
|
|
162415
|
+
function zeroTouchHandled(view) {
|
|
162416
|
+
const m2 = model5(view);
|
|
162417
|
+
return card({ title: "Answered from a draft", body: m2.state !== "ready" ? fail5(m2) : html`${kpi(m2.totals.draftAnswers, "runs answered in the covered interval")}<p class="zt-kps">${ZERO_TOUCH_WORDS.drafts}</p>${m2.coverage.partial ? html`<p class="zt-kps" role="status">${ZERO_TOUCH_WORDS.draftsPartial}</p>` : null}` });
|
|
162418
|
+
}
|
|
162419
|
+
function meter3(row2) {
|
|
162420
|
+
if (row2.rate === null) return html`<span class="zt-m">Not enough history</span>`;
|
|
162421
|
+
const tone = row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
162422
|
+
const word3 = row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
162423
|
+
return html`<span class="zt-m"><svg class="${`pbar tone-${tone}`}" height="10" role="meter" aria-valuenow="${svgNum(row2.rate, 0, 100)}" aria-valuemin="0" aria-valuemax="100" aria-label="${`${row2.name}: ${pct2(row2.rate)} of runs needed a person, goal under 5 percent; meter scale 40 percent`}"><rect class="pbar__bed" width="100%" height="10" rx="5"/><rect class="pbar__fill" width="${`${svgNum(row2.rate / 40 * 100, 0, 100)}%`}" height="10" rx="5"/><line class="zt-goal" x1="12.5%" x2="12.5%" y1="-4" y2="14"/></svg><span class="zt-mv"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span></span>`;
|
|
162424
|
+
}
|
|
162425
|
+
function rowContent(row2, sub2 = false) {
|
|
162426
|
+
const c = causesByKey.get(row2.topCause);
|
|
162427
|
+
return html`<span class="zt-chev">${sub2 ? null : icon("chev-r", { size: "sm" })}</span><span class="zt-name"><b>${row2.name}</b><small>${sub2 ? `${row2.runs} runs` : row2.repositories.map((r2) => r2.name).join(" and ")}</small></span>${meter3(row2)}${delta(row2.deltaPoints, row2.previousRate)}<span class="zt-n"><b>${row2.touched}</b> of ${row2.runs} runs</span><span class="${`zt-st${row2.waitingNow ? " tone-parked" : ""}`}">${icon(row2.waitingNow ? "parked" : "flag", { size: "sm" })}${row2.waitingPartial ? "waiting history incomplete" : row2.waitingNow ? "waiting now" : row2.zeroTouch ? "zero touch, 30+ days" : row2.streakDays === null ? "not enough history" : `${amount3(row2.streakDays)} ${row2.streakDays === 1 ? "day" : "days"}`}</span><span class="zt-c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : "No touches"}</span>`;
|
|
162428
|
+
}
|
|
162429
|
+
function phoneProjects(m2, view) {
|
|
162430
|
+
const item2 = (row2, repository = false) => {
|
|
162431
|
+
const c = causesByKey.get(row2.topCause), tone = row2.rate === null ? "idle" : row2.rate < 5 ? "ok" : row2.rate < 30 ? "attention" : "failed";
|
|
162432
|
+
const word3 = row2.rate === null ? "Not known" : row2.rate < 5 ? "Under goal" : row2.rate < 30 ? "Above goal" : "High";
|
|
162433
|
+
const query = new URLSearchParams({ days: String(m2.windowDays), ...row2.project ? { project: row2.project } : {}, ...repository && row2.scopeId ? { scope: row2.scopeId } : {} });
|
|
162434
|
+
return html`<li><a class="ztp-p" href="${`/runs/zero-touch?${query}`}" aria-label="${`${row2.name}: ${pct2(row2.rate)}, open ${repository ? "repository" : "project"}`}"${repository ? html` data-repository` : null}><span class="ztp-p__n"><b>${row2.name}</b><small>${row2.touched} of ${row2.runs} runs</small></span><span class="ztp-p__r"><b>${pct2(row2.rate)}</b><span class="${`zt-lv tone-${tone}`}">${icon(tone === "ok" ? "done" : "attention", { size: "sm" })}${word3}</span></span><span class="ztp-p__c">${c ? html`<span class="${`zt-cause tone-${c.tone}`}">${icon(c.icon, { size: "sm" })}${c.short}</span>` : html`<span>No touches</span>`}${delta(row2.deltaPoints, row2.previousRate)}</span><span class="ztp-p__go">${icon("chev-r", { size: "sm" })}</span></a></li>`;
|
|
162435
|
+
};
|
|
162436
|
+
return html`<ul class="ztp-ps zt-phone-projects">${m2.projects.map((p) => html`${item2(p)}${view?.query?.project === p.project ? p.repositories.map((r2) => item2(r2, true)) : null}`)}</ul>`;
|
|
162437
|
+
}
|
|
162438
|
+
function zeroTouchProjects(view) {
|
|
162439
|
+
const m2 = model5(view);
|
|
162440
|
+
if (m2.state !== "ready") return card({ title: "By project", body: fail5(m2) });
|
|
162441
|
+
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>`;
|
|
162442
|
+
const rows3 = m2.projects.map((p) => html`<li><details class="zt-p"${view?.query?.project === p.project ? html` open` : null}><summary class="zt-row" aria-label="${`${p.name}: ${pct2(p.rate)}, show its repositories or folders`}">${rowContent(p)}</summary><ul class="zt-subs">${p.repositories.map((r2) => html`<li class="zt-row zt-row--sub">${rowContent(r2, true)}</li>`)}</ul>${acts3(view) && p.touchedRunIds.length ? html`<p class="zt-foot"><a class="link" href="${`/runs?${new URLSearchParams({ project: p.project ?? "", touched: "1", days: String(m2.windowDays) })}`}">Open touched runs</a></p>` : null}</details></li>`);
|
|
162443
|
+
return card({ title: "By project", sub: `${m2.totals.touched} of ${m2.totals.runs} runs needed a person`, flush: true, body: html`${fail5(m2)}${m2.projects.length ? html`<div class="zt-tbl zt-desktop-projects">${head}<ul class="zt-list">${rows3}</ul></div>${phoneProjects(m2, view)}<p class="zt-foot">${icon("info", { size: "sm" })}<span>A row opens to its repositories or folders. A run counts once, however many interventions it received.</span></p>` : html`<p class="zt-note">${ZERO_TOUCH_WORDS.empty}</p>`}` });
|
|
162444
|
+
}
|
|
162445
|
+
function causeCounts(values) {
|
|
162446
|
+
return ZERO_TOUCH_CAUSES.map((c) => ({ ...c, count: values.find((v) => v.cause === c.cause)?.count ?? 0 }));
|
|
162447
|
+
}
|
|
162448
|
+
function causeBar(values, max, name2) {
|
|
162449
|
+
const rows3 = causeCounts(values);
|
|
162450
|
+
let at2 = 0;
|
|
162451
|
+
max = Math.max(1, max, rows3.reduce((sum, row2) => sum + row2.count, 0));
|
|
162452
|
+
const all = rows3.filter((r2) => r2.count > 0);
|
|
162453
|
+
const label3 = `${name2}: ${all.map((r2) => `${r2.label} ${r2.count}`).join(", ") || "no touches"}; full scale ${max} touched runs`;
|
|
162454
|
+
return html`<svg class="zt-cause-svg" width="100%" height="28" role="img" aria-label="${label3}">${all.map((r2) => {
|
|
162455
|
+
const x2 = at2 / Math.max(1, max) * 500, w = r2.count / Math.max(1, max) * 500;
|
|
162456
|
+
at2 += r2.count;
|
|
162457
|
+
return html`<g class="${`tone-${r2.tone}`}"><rect class="zt-cause-rect" x="${`${svgNum(x2 / 5, 0, 100)}%`}" y="0" width="${`${svgNum(w / 5, 0, 100)}%`}" height="28" rx="5"/>${w >= 22 ? html`<text class="zt-cause-label" x="${`${svgNum((x2 + w / 2) / 5, 0, 100)}%`}" y="18" text-anchor="middle">${r2.count}</text>` : null}</g>`;
|
|
162458
|
+
})}</svg>`;
|
|
162459
|
+
}
|
|
162460
|
+
function zeroTouchCauses(view) {
|
|
162461
|
+
const m2 = model5(view), max = Math.max(1, ...m2.projects.map((p) => Math.max(p.touched, causeCounts(p.causes).reduce((sum, row2) => sum + row2.count, 0))));
|
|
162462
|
+
if (m2.state !== "ready") return card({ title: "What needed a person", body: fail5(m2) });
|
|
162463
|
+
const bars = m2.projects.map((p) => html`<li class="zt-cb"><span class="zt-cb__n"><b>${p.name}</b><small>${p.touched} of ${p.runs}</small></span>${causeBar(p.causes, max, p.name)}<p class="zt-cb__t">${causeCounts(p.causes).filter((c) => c.count).map((c) => `${c.short} ${c.count}`).join(" \xB7 ") || "No touches"}</p></li>`);
|
|
162464
|
+
const scopeLabel = view?.query?.scope ? "selected repository" : view?.query?.project ? "selected project" : "all projects";
|
|
162465
|
+
return card({ title: "What needed a person", sub: html`<span class="zt-desktop-label">last ${m2.windowDays} days, by project</span><span class="zt-phone-label">${m2.windowDays} days, ${scopeLabel}</span>`, body: html`${fail5(m2)}<ul class="zt-bars zt-desktop-causes">${bars}</ul><div class="zt-phone-causes">${causeBar(m2.causes, Math.max(1, m2.totals.touched), scopeLabel === "all projects" ? "All projects" : scopeLabel)}</div><ul class="zt-whys">${causeCounts(m2.causes).map((c) => html`<li class="zt-why">${tile(c.tone === "running" ? "running" : c.tone === "idle" ? "idle" : c.tone, { icon: c.icon, sm: true })}<span><b>${c.label}</b><small>${c.detail}</small></span><span class="zt-why__n">${c.count}</span></li>`)}<li class="zt-why zt-why--sum"><span></span><span><b>Runs that needed a person</b></span><span class="zt-why__n">${m2.totals.touched}</span></li></ul><p class="zt-merge">${icon("pr", { size: "sm" })}<span>Merging is never a touch. <a class="link" href="#zt-counting">How touches are counted</a></span></p><details class="zt-counting" id="zt-counting"><summary>How touches are counted</summary><p class="zt-kps">The rate counts workload runs with a retained record that ended in the covered interval. Each run counts once. Bars assign it to its first successful human intervention; later interventions remain in history. Jobs stopped before starting are separate. Reads, sandbox opens, automatic starts and internal grouping jobs do not enter the touch count.</p></details>` });
|
|
162466
|
+
}
|
|
162467
|
+
function guardedForm(action2, { hidden: hidden2 = [], fields: fields3 = null, label: label3, kind = ["sm"] } = {}) {
|
|
162468
|
+
return html`<form class="act zt-action" data-action="${action2}" autocomplete="off"><fieldset data-config-controls disabled>${hidden2.map(([name2, value, type]) => html`<input type="hidden" name="${name2}" value="${value}"${type ? html` data-type="${type}"` : null}>`)}${fields3}${btn({ type: "submit", kind, label: label3 })}</fieldset><noscript><p class="zt-kps">${ZERO_TOUCH_NO_SCRIPT}</p></noscript><p class="field__err" data-form-error hidden role="alert"></p></form>`;
|
|
162469
|
+
}
|
|
162470
|
+
function nameForm(g, index2) {
|
|
162471
|
+
const key2 = `zt-name-${index2}`;
|
|
162472
|
+
return html`<details class="zt-editor"><summary class="link">${g.name ? "Rename this group" : "Name this group"}</summary>${guardedForm("zero-touch-name", { hidden: [["groupId", g.id]], fields: field2({ label: "Group name", id: key2, control: input({ id: key2, name: "name", value: g.name, required: true }), hint: "Write your own name, up to 120 characters. Question text is read only on its run page." }), label: "Preview name" })}</details>`;
|
|
162473
|
+
}
|
|
162474
|
+
function instructionForm(g, index2) {
|
|
162475
|
+
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>`;
|
|
162476
|
+
const key2 = `zt-sentence-${index2}`;
|
|
162477
|
+
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>`;
|
|
162478
|
+
}
|
|
162479
|
+
function zeroTouchQuestions(view) {
|
|
162480
|
+
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." });
|
|
162481
|
+
const m2 = model5(view);
|
|
162482
|
+
if (m2.state !== "ready") return card({ title: "Questions it keeps asking", body: fail5(m2) });
|
|
162483
|
+
const rows3 = m2.groups.map((g, index2) => html`<li class="zt-q"><div class="zt-q__h"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><div><p class="${`zt-q__t${g.name ? "" : " zt-q__t--un"}`}">${g.name ?? `${g.count} similar questions, not named yet`}</p><p class="zt-q__s"><span class="code">${g.repo ?? g.project ?? "Repository not known"}</span>${g.status === "fixed" && g.fixedAt !== null ? html` · fixed ${date(g.fixedAt)} UTC` : null}${g.status === "fixed" && m2.fixes.some((f3) => f3.groupId === g.id && f3.afterCount === 0 && f3.subsequentRuns > 0 && !f3.partial && f3.undoneAt === null) ? html` · none since` : null}</p></div><span class="zt-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span></div><p class="zt-said">${icon("runs", { size: "sm" })}<span>Asked in ${g.runIds.slice(0, 8).map((id7, at2) => html`${at2 ? ", " : ""}${link(id7)}`)}${g.runIds.length > 8 ? ` and ${g.runIds.length - 8} more` : null}.</span></p>${acts3(view) ? html`<div class="zt-q__act">${nameForm(g, index2)}${g.status !== "fixed" ? instructionForm(g, index2) : html`<a class="link" href="#p-zt-fixes">See the fix</a>`}</div>` : null}</li>`);
|
|
162484
|
+
const phoneRows = m2.groups.map((g, index2) => html`<li class="ztp-q"><span class="zt-q__n" aria-label="${`asked ${g.count} times`}">${g.count}×</span><span class="ztp-q__t"><b>${g.runIds[0] ? html`<a class="zt-group-link" href="${`/run?id=${encodeURIComponent(g.runIds[0])}`}">${g.name ?? `${g.count} similar, not named yet`}</a>` : g.name ?? `${g.count} similar, not named yet`}</b><small>${g.repo ?? g.project ?? "Repository not known"}</small></span><span class="ztp-q__st">${chip(g.status === "fixed" ? "Fixed" : g.status === "unassigned" ? "Unassigned" : "Open", { status: g.status === "fixed" ? "ok" : "parked", icon: g.status === "fixed" ? "done" : "parked" })}</span><span class="ztp-q__a">${acts3(view) ? g.status === "fixed" ? html`<a class="link" href="#p-zt-fixes">See the fix</a>` : g.name ? instructionForm(g, `phone-${index2}`) : nameForm(g, `phone-${index2}`) : null}</span></li>`);
|
|
162485
|
+
const sources = m2.grouping.sources.length ? m2.grouping.sources : m2.ungroupedRunIds.map((runId2) => ({ runId: runId2, status: "unassigned" }));
|
|
162486
|
+
return card({ title: "Questions it keeps asking", sub: "operator only", body: html`<p class="zt-intro">Grouped by meaning. You name each group; read the questions on their run pages.</p>${fail5(m2)}${m2.grouping.partial ? html`<p class="zt-note" role="status">Question grouping is partial. Missing or unclassified sources are not evidence that a question stopped recurring.</p>` : null}${["unavailable", "configuration-unavailable"].includes(m2.grouping.state) ? html`<p class="zt-note" role="status">${m2.grouping.state === "configuration-unavailable" ? "Grouping configuration could not be read. Check the reviewed triggers file before starting a grouping job." : "Question groups could not be read. Reload to retry; missing assignments are not an empty group list."}</p>` : null}${rows3.length ? html`<ol class="zt-qs zt-desktop-questions">${rows3}</ol><ol class="ztp-qs zt-phone-questions">${phoneRows}</ol>` : html`<p class="zt-kps">${["unavailable", "configuration-unavailable"].includes(m2.grouping?.state) ? "Grouping coverage is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noGroups}</p>`}${sources.length ? html`<ul class="zt-ungrouped">${sources.slice(0, 20).map((source2) => html`<li data-group-status="${source2.status}">${link(source2.runId)}<span>${ZERO_TOUCH_SOURCE_WORDS[source2.status]}</span>${acts3(view) && ["unassigned", "stale", "refused", "failed"].includes(source2.status) ? guardedForm("zero-touch-group", { hidden: [["runId", source2.runId]], label: "Group question", kind: ["sm", "paid"] }) : null}</li>`)}</ul>` : null}<p class="zt-foot zt-foot--in">${icon("info", { size: "sm" })}<span>${m2.grouping.enabled ? `${plural5(m2.grouping.pending, "grouping job")} pending; ${plural5(m2.grouping.unassigned, "question")} unassigned. Expired or unavailable context stays ungrouped.` : "Private capture and a capped classifier must be enabled in the reviewed zeroTouch configuration before questions can be grouped."}</span></p>` });
|
|
162487
|
+
}
|
|
162488
|
+
function fixTone(f3) {
|
|
162489
|
+
if (f3.partial || f3.subsequentRuns === 0 || f3.undoneAt !== null || f3.status === "observing") return "idle";
|
|
162490
|
+
return f3.status === "improved" ? "ok" : f3.status === "worse" ? "failed" : "idle";
|
|
162491
|
+
}
|
|
162492
|
+
function fixObservation(f3) {
|
|
162493
|
+
if (f3.undoneAt !== null) return "Reverted: this observation does not establish current effectiveness.";
|
|
162494
|
+
if (f3.partial) return "Partial observation: effectiveness is not established.";
|
|
162495
|
+
if (f3.subsequentRuns === 0) return "No subsequent runs yet: effectiveness is not established.";
|
|
162496
|
+
return f3.status === "improved" ? "Fewer occurrences over equal observed spans." : f3.status === "worse" ? "More occurrences over equal observed spans." : f3.status === "unchanged" ? "The same number of occurrences over equal observed spans." : "Still observing the targeted cause.";
|
|
162497
|
+
}
|
|
162498
|
+
function beforeAfter(label3, value, max, tone = "attention") {
|
|
162499
|
+
return html`<p class="zt-ba"><span>${label3}</span><svg class="${`pbar tone-${tone}`}" height="8" role="img" aria-label="${`${label3}: ${value === null ? "not known" : value}`}"><rect class="pbar__bed" width="100%" height="8" rx="4"/>${value ? html`<rect class="pbar__fill" width="${`${svgNum(value / max * 100, 0, 100)}%`}" height="8" rx="4"/>` : null}</svg><b>${value ?? "?"}</b></p>`;
|
|
162500
|
+
}
|
|
162501
|
+
function zeroTouchFixes(view) {
|
|
162502
|
+
const m2 = model5(view);
|
|
162503
|
+
const evidence = html`${m2.fixesPartial ? html`<p class="zt-note" role="status">Fix history is incomplete. Reload to retry; missing changes are not evidence that no fix was made.</p>` : null}${m2.fixes.length ? html`<ol class="zt-fxs">${m2.fixes.map((f3) => html`<li class="zt-fx"><p class="zt-fx__d">${date(f3.at)} UTC${f3.undoneAt === null ? null : html` · Reverted ${date(f3.undoneAt)} UTC`}</p><p class="zt-fx__w">${f3.kind === "instructions" ? `Trigger ${f3.triggerIndex === null ? "not known" : f3.triggerIndex + 1}'s instructions gained a sentence` : "Configuration changed to address a refusal"}</p><p class="zt-fx__m">${f3.reason ? `${f3.reason} refusals` : "Questions in the targeted group"}, equal ${span(f3.spanMs)} spans before and after; ${f3.subsequentRuns} workload runs after the fix.</p>${beforeAfter("Before", f3.beforeCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1))}${beforeAfter("After", f3.afterCount, Math.max(f3.beforeCount ?? 0, f3.afterCount ?? 0, 1), fixTone(f3))}<p class="zt-kps">${fixObservation(f3)}</p>${acts3(view) && f3.kind === "instructions" && f3.undoneAt === null ? guardedForm("zero-touch-undo", { hidden: [["fixId", f3.id]], label: "Preview removing the sentence", kind: ["sm", "ghost"] }) : null}</li>`)}</ol>` : html`<p class="zt-kps">${m2.fixesPartial ? "Fix history is not known. Reload after the source is available." : ZERO_TOUCH_WORDS.noFixes}</p>`}`;
|
|
162504
|
+
const brief = m2.fixes.map((f3) => `${f3.reason ?? "grouped questions"}: ${f3.beforeCount ?? "?"} to ${f3.afterCount ?? "?"}`).join("; ");
|
|
162505
|
+
return card({ title: "Fixes that worked", sub: "what changed, and what it did", body: html`<div class="zt-desktop-fixes">${evidence}</div><details class="zt-phone-fixes"><summary>${icon("done", { size: "sm" })}<span><b>${m2.fixesPartial ? "Fix history incomplete" : `${m2.fixes.length} fixes recorded`}</b>${brief ? `: ${brief}. ` : ". "}<span class="link">Open observations</span></span></summary><div class="zt-fix-observations">${evidence}</div></details>` });
|
|
162506
|
+
}
|
|
162507
|
+
var ZERO_TOUCH_WORDS, ZERO_TOUCH_CAUSES, ZERO_TOUCH_SOURCE_WORDS, causesByKey, model5, acts3, pct2, amount3, date, span, link, fail5, ZERO_TOUCH_NO_SCRIPT, ZERO_TOUCH_PANELS;
|
|
162508
|
+
var init_zero_touch = __esm({
|
|
162509
|
+
"src/console/panels/zero-touch.mjs"() {
|
|
162510
|
+
init_common();
|
|
162511
|
+
init_html();
|
|
162512
|
+
init_kit();
|
|
162513
|
+
init_zero_touch_view();
|
|
162514
|
+
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." });
|
|
162515
|
+
ZERO_TOUCH_CAUSES = Object.freeze([
|
|
162516
|
+
{ cause: "question-answer", label: "Questions", short: "Questions", icon: "parked", tone: "parked", detail: "A person answered a parked question and the source session resumed." },
|
|
162517
|
+
{ cause: "approval-decision", label: "Approvals", short: "Approvals", icon: "shield", tone: "attention", detail: "A person approved or declined a request. System expiry is excluded." },
|
|
162518
|
+
{ 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." },
|
|
162519
|
+
{ 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." },
|
|
162520
|
+
{ cause: "steer", label: "Steered", short: "Steered", icon: "steer", tone: "running", detail: "A person's steering message was delivered to the run." },
|
|
162521
|
+
{ cause: "cancel", label: "Cancelled", short: "Cancelled", icon: "cancel", tone: "idle", detail: "A person cancelled the run while it was active." }
|
|
162522
|
+
]);
|
|
162523
|
+
ZERO_TOUCH_SOURCE_WORDS = Object.freeze({
|
|
162524
|
+
unassigned: "Not grouped yet.",
|
|
162525
|
+
pending: "A grouping job is pending.",
|
|
162526
|
+
assigned: "Assigned to a question group.",
|
|
162527
|
+
"source-expired": "The retained question context expired. Open the run to read what remains; a new grouping job cannot restore it.",
|
|
162528
|
+
"source-unavailable": "The worker could not retain safe question context. Check transcript capture on the source host.",
|
|
162529
|
+
"source-oversize": "The private question context exceeds its byte limit. It remains ungrouped rather than being cut.",
|
|
162530
|
+
"candidates-expired": "A group's representative context expired. Review the groups before trying again.",
|
|
162531
|
+
"group-limit": "The repository reached the bounded group limit. Review existing groups before trying again.",
|
|
162532
|
+
stale: "The group revision changed during classification. Preview a fresh grouping job.",
|
|
162533
|
+
refused: "The grouping job was refused. Open its run to see the policy reason.",
|
|
162534
|
+
failed: "The grouping job failed. Open its run and review the failure before trying again."
|
|
162535
|
+
});
|
|
162536
|
+
causesByKey = new Map(ZERO_TOUCH_CAUSES.map((c) => [c.cause, c]));
|
|
162537
|
+
model5 = (v) => zeroTouchView(v?.data?.zeroTouch, { role: v?.role, windowDays: v?.query?.days ?? 30, nowMs: v?.now ?? 0 });
|
|
162538
|
+
acts3 = (v) => v?.role === "operator" && !v?.readOnly;
|
|
162539
|
+
pct2 = (n3) => Number.isFinite(n3) ? `${Math.round(n3)}%` : "Not known";
|
|
162540
|
+
amount3 = (n3) => Number.isFinite(n3) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(n3) : "Not known";
|
|
162541
|
+
date = (n3) => Number.isFinite(n3) && n3 >= 0 && n3 <= 864e13 ? new Date(n3).toISOString().slice(0, 10) : "not known";
|
|
162542
|
+
span = (ms3) => ms3 === null ? "not known" : ms3 >= 36e5 ? `${amount3(ms3 / 36e5)}h` : `${amount3(ms3 / 6e4)}m`;
|
|
162543
|
+
link = (id7) => html`<a class="link code" href="${`/run?id=${encodeURIComponent(id7)}`}">${id7}</a>`;
|
|
162544
|
+
fail5 = (m2) => m2.state === "unavailable" ? html`<p class="zt-note" role="status">${ZERO_TOUCH_WORDS.unavailable}</p>` : null;
|
|
162545
|
+
ZERO_TOUCH_NO_SCRIPT = "Enable JavaScript to preview and confirm a change. Reading and navigation work without it.";
|
|
162546
|
+
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]]);
|
|
162547
|
+
}
|
|
162548
|
+
});
|
|
162549
|
+
|
|
162550
|
+
// src/trigger-policy.mjs
|
|
162551
|
+
function triggerWhoMayFire(trigger2) {
|
|
162552
|
+
const on = trigger2.on ?? trigger2;
|
|
162553
|
+
const forge = trigger2.run?.kind ?? trigger2.forge;
|
|
162554
|
+
const kind = on.type;
|
|
162555
|
+
if (kind === "cron") return "This deployment's scheduler starts it on its schedule; an operator may fire it by hand.";
|
|
162556
|
+
if (forge === "gitlab") return "The actor must be a GitLab project member with Developer access (30) or higher, including inherited membership.";
|
|
162557
|
+
if (forge === "forgejo") return "The actor must have Forgejo repository owner, admin or write permission.";
|
|
162558
|
+
if (forge === "azure") return "The actor must be an Azure DevOps project member admitted by the receiver's membership check.";
|
|
162559
|
+
if (kind === "label") return "A collaborator applies the matching issue label; the label event supplies the approval.";
|
|
162560
|
+
if (kind === "comment") return "The commenter's GitHub association must be OWNER, MEMBER or COLLABORATOR; the service's own comments are refused.";
|
|
162561
|
+
const actions = Array.isArray(on.action) ? on.action : [];
|
|
162562
|
+
const gates = [];
|
|
162563
|
+
if (kind === "issue" || actions.includes("closed")) gates.push("The closer must have repository write access");
|
|
162564
|
+
if (actions.includes("labeled")) gates.push("A collaborator-applied matching label supplies approval");
|
|
162565
|
+
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");
|
|
162566
|
+
if (actions.some((a) => ["opened", "synchronize", "reopened"].includes(a))) gates.push("The pull request author's association must be OWNER, MEMBER or COLLABORATOR");
|
|
162567
|
+
return `${gates.join(". ") || "The receiver checks the actor's authority for the configured event"}.`;
|
|
162568
|
+
}
|
|
162569
|
+
var init_trigger_policy = __esm({
|
|
162570
|
+
"src/trigger-policy.mjs"() {
|
|
162254
162571
|
}
|
|
162255
162572
|
});
|
|
162256
162573
|
|
|
@@ -163425,7 +163742,7 @@ function projectSizing2(v) {
|
|
|
163425
163742
|
const s2 = p.sizing;
|
|
163426
163743
|
const dim = (label3, d, words) => html`<p class="proj-verdict"><b>${label3}: ${d?.suggested ? `suggest ${label3 === "Memory" ? gibText(d.suggested) : formatCpus(d.suggested) + " CPUs"}` : d?.reason === "not-enough-runs" ? "not enough runs" : d?.reason ?? "unknown"}.</b> ${(label3 === "Memory" ? memoryWords(words) : words) ?? "No suggestion could be read."}</p>`;
|
|
163427
163744
|
const action2 = s2?.action;
|
|
163428
|
-
return card6("Job size", html`${projectSizingChart(p)}${s2 ? html`${dim("Memory", s2.memory, s2.words?.memory)}${dim("CPUs", s2.cpu, s2.words?.cpu)}${[memoryWords(s2.words?.memoryHeld), s2.words?.memoryFact, s2.words?.cpuFact, memoryWords(s2.words?.refusal)].filter(Boolean).map(note2)}` : note2("No size suggestion could be read.")}${p.sizingUnreadable ? note2("The measurements or scoped limits could not be read completely.") : null}${note2("Measured inside the jobs, so advisory.")}${s2?.call && canAct(v) ? html`<p class="proj-sub">The exact call</p><div class="cmd"><code>${s2.call}</code><button class="btn btn--sm" type="button" data-copy hidden>Copy</button></div>${action2 ? note2(action2.row === null ? `Adds a scoped-limits row for project:${p.id}.` : `Applies to scoped-limits row ${action2.row}, project:${p.id}.`) : null}${canAct(v) && action2 ? actForm3(action2.action, { hidden: [...action2.index === void 0 ? [] : [["index", action2.index, "int"]], ["limit", JSON.stringify(action2.limit), "json"]], label: "Preview this size" }) : null}` : null}`, "peak memory per run, last 30 days");
|
|
163745
|
+
return card6("Job size", html`${projectSizingChart(p)}${s2 ? html`${dim("Memory", s2.memory, s2.words?.memory)}${dim("CPUs", s2.cpu, s2.words?.cpu)}${[memoryWords(s2.words?.memoryHeld), s2.words?.memoryFact, s2.words?.cpuFact, memoryWords(s2.words?.refusal), s2.words?.memoryCeiling].filter(Boolean).map(note2)}` : note2("No size suggestion could be read.")}${p.sizingUnreadable ? note2("The measurements or scoped limits could not be read completely.") : null}${note2("Measured inside the jobs, so advisory.")}${s2?.call && canAct(v) ? html`<p class="proj-sub">The exact call</p><div class="cmd"><code>${s2.call}</code><button class="btn btn--sm" type="button" data-copy hidden>Copy</button></div>${action2 ? note2(action2.row === null ? `Adds a scoped-limits row for project:${p.id}.` : `Applies to scoped-limits row ${action2.row}, project:${p.id}.`) : null}${canAct(v) && action2 ? actForm3(action2.action, { hidden: [...action2.index === void 0 ? [] : [["index", action2.index, "int"]], ["limit", JSON.stringify(action2.limit), "json"]], label: "Preview this size" }) : null}` : null}`, "peak memory per run, last 30 days");
|
|
163429
163746
|
}
|
|
163430
163747
|
function projectRecent(v) {
|
|
163431
163748
|
if (!ready(v)) return null;
|
|
@@ -172194,6 +172511,14 @@ var init_watch = __esm({
|
|
|
172194
172511
|
}
|
|
172195
172512
|
});
|
|
172196
172513
|
|
|
172514
|
+
// src/console/restart-notice.mjs
|
|
172515
|
+
var init_restart_notice = __esm({
|
|
172516
|
+
"src/console/restart-notice.mjs"() {
|
|
172517
|
+
init_kit();
|
|
172518
|
+
init_html();
|
|
172519
|
+
}
|
|
172520
|
+
});
|
|
172521
|
+
|
|
172197
172522
|
// src/console/session-ends.mjs
|
|
172198
172523
|
function streamEndOf(v) {
|
|
172199
172524
|
return typeof v === "string" && STREAM_ENDS.includes(v) ? v : null;
|
|
@@ -172260,6 +172585,7 @@ var init_shell = __esm({
|
|
|
172260
172585
|
init_html();
|
|
172261
172586
|
init_kit();
|
|
172262
172587
|
init_pages();
|
|
172588
|
+
init_restart_notice();
|
|
172263
172589
|
init_pulse_icons();
|
|
172264
172590
|
init_signin();
|
|
172265
172591
|
init_theme();
|
|
@@ -174616,6 +174942,15 @@ import { createHash as createHash28 } from "node:crypto";
|
|
|
174616
174942
|
function changesHtml(changes) {
|
|
174617
174943
|
return (Array.isArray(changes) ? changes : []).map(([key2, before, after], i2) => html`${i2 > 0 ? ", " : null}${key2}: <b>${before}</b> to <b>${after}</b>`);
|
|
174618
174944
|
}
|
|
174945
|
+
function plainBody(value, depth = 0) {
|
|
174946
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
174947
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
174948
|
+
if (depth >= BODY_DEPTH_MAX || typeof value !== "object") return false;
|
|
174949
|
+
if (Array.isArray(value)) return value.length <= 256 && value.every((v) => plainBody(v, depth + 1));
|
|
174950
|
+
const keys3 = Object.keys(value);
|
|
174951
|
+
if (keys3.length > BODY_KEYS_MAX) return false;
|
|
174952
|
+
return keys3.every((k) => plainBody(value[k], depth + 1));
|
|
174953
|
+
}
|
|
174619
174954
|
function str4(body, key2, { required = false, max = 512 } = {}) {
|
|
174620
174955
|
const v = body?.[key2];
|
|
174621
174956
|
if (v === void 0 || v === null || v === "") {
|
|
@@ -175245,11 +175580,37 @@ function settingDone(out) {
|
|
|
175245
175580
|
const sayable = typeof old === "string" && old.trim() !== "" || typeof old === "number" && Number.isFinite(old);
|
|
175246
175581
|
return sayable ? `Changed ${key2} from ${String(old)} to ${value}` : `Changed ${key2} to ${value}`;
|
|
175247
175582
|
}
|
|
175583
|
+
function pauseListShapeRefusal(windows) {
|
|
175584
|
+
for (const [i2, w] of windows.entries()) {
|
|
175585
|
+
const at2 = atIndex("pause window", i2);
|
|
175586
|
+
if (w === null || typeof w !== "object" || Array.isArray(w)) return `${at2} is not an object`;
|
|
175587
|
+
for (const [k, value] of Object.entries(w)) {
|
|
175588
|
+
if (!PAUSE_WINDOW_FIELDS.includes(k)) return `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only)`;
|
|
175589
|
+
const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
|
|
175590
|
+
if (!plain) return `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}`;
|
|
175591
|
+
}
|
|
175592
|
+
}
|
|
175593
|
+
return null;
|
|
175594
|
+
}
|
|
175248
175595
|
function pauseListBack(out) {
|
|
175249
175596
|
const unread2 = typeof out.beforeText === "string" && !Array.isArray(jsonOf(out.beforeText)?.windows);
|
|
175250
175597
|
if (unread2) return { undo: twice(UNDO_SENTENCES.tooBig, UNDO_SENTENCES.tooBigAfter), inverse: null };
|
|
175598
|
+
if (!pauseListGoesBack(out)) return { undo: twice(UNDO_SENTENCES.refusedList, UNDO_SENTENCES.refusedListAfter), inverse: null };
|
|
175251
175599
|
return { undo: "Put the previous pause windows back", inverse: { action: "pause-windows", body: { windows: out.extra.before } } };
|
|
175252
175600
|
}
|
|
175601
|
+
function pauseListGoesBack(out) {
|
|
175602
|
+
const before = out.extra?.before;
|
|
175603
|
+
if (!Array.isArray(before) || !plainBody({ windows: before }) || pauseListShapeRefusal(before) !== null) return false;
|
|
175604
|
+
if (typeof out.beforeText !== "string") return true;
|
|
175605
|
+
const held = heldPauseWindows(out.beforeText, out.target ?? "pause-windows.json");
|
|
175606
|
+
if (held.problem) return false;
|
|
175607
|
+
try {
|
|
175608
|
+
parsePauseWindows(pauseWindowsText(held.doc, before), out.target ?? "pause-windows.json");
|
|
175609
|
+
return true;
|
|
175610
|
+
} catch {
|
|
175611
|
+
return false;
|
|
175612
|
+
}
|
|
175613
|
+
}
|
|
175253
175614
|
function triggerEditBack(out) {
|
|
175254
175615
|
const p = out.plan ?? {};
|
|
175255
175616
|
const n3 = atIndex("trigger", p.index);
|
|
@@ -175344,7 +175705,7 @@ function adoptable(facts7, { name: name2, version }) {
|
|
|
175344
175705
|
function actionNamed(name2) {
|
|
175345
175706
|
return ACTIONS3.find((a) => a.name === name2) ?? null;
|
|
175346
175707
|
}
|
|
175347
|
-
var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
|
|
175708
|
+
var BULK_MAX2, CANCEL_TIMEOUT_SENTENCE, JOB_ID_RE2, BODY_DEPTH_MAX, BODY_KEYS_MAX, sha2563, digestOf, codeOf4, AGENT_POLICY_TITLES, rerunAuthorityDigest, atIndex, BadRequest, UNCHANGED, reserveTail, SNAPSHOT_TIMEOUT_MS, SNAPSHOT_TIMEOUT_SENTENCE, SWITCH_READ, SNAPSHOT_UNREAD_SENTENCE, SNAPSHOT_LATE, FILE_NEEDS, JOB_NEEDS, FLEET_NEEDS, TRIGGER_FORM, settingBar, pausePath, shownDir, pauseWindowsWhole, writePause, writeTriggersOf, writeSettingsOf, writeLimitsOf, writeProjectsOf, configActions, failedLine2, drawable, CANCEL_QUEUED_LINE, jobActions, SWITCH_QUEUES_NAMED, SWITCH_SHORT_LINES, queueUnreadRefusal, RESUME_WAITING_UNKNOWN, fleetActions, UNDO_SENTENCES, twice, DELETED, resumeManyUndo, listBefore, byHand, say, none, confirmWord, rerunCancel, replacedOne, whereOf2, bulkDone, PAUSE_FIELDS, WRITE_RULES, PAUSE_EDIT, LIMIT_EDIT, RULE_FNS, zeroTouchActions, modelRoleActions, triggerEnvironmentActions, sandboxActions, deviceActions, boardActions, stagedRow, packageActions, CONFIG_FIX_ACTIONS, ACTIONS3;
|
|
175348
175709
|
var init_actions = __esm({
|
|
175349
175710
|
"src/console/actions.mjs"() {
|
|
175350
175711
|
init_package_canary_record();
|
|
@@ -175381,6 +175742,8 @@ var init_actions = __esm({
|
|
|
175381
175742
|
BULK_MAX2 = 100;
|
|
175382
175743
|
CANCEL_TIMEOUT_SENTENCE = "no worker acknowledged: the job may have just finished, gone back to the queue (cancel it again to remove it), or its host is unreachable; nothing was changed";
|
|
175383
175744
|
JOB_ID_RE2 = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
175745
|
+
BODY_DEPTH_MAX = 8;
|
|
175746
|
+
BODY_KEYS_MAX = 64;
|
|
175384
175747
|
sha2563 = (text4) => createHash28("sha256").update(text4).digest("hex");
|
|
175385
175748
|
digestOf = (text4) => text4 === null || text4 === void 0 ? null : sha2563(text4);
|
|
175386
175749
|
codeOf4 = (e2) => typeof e2?.code === "string" && /^[A-Z0-9_]{1,40}$/.test(e2.code) ? e2.code : "error";
|
|
@@ -175411,15 +175774,8 @@ var init_actions = __esm({
|
|
|
175411
175774
|
needs: [],
|
|
175412
175775
|
async preview(body, ctx) {
|
|
175413
175776
|
if (!Array.isArray(body.windows)) return { refusal: 'send {"windows": [...]} with the whole new list of pause windows' };
|
|
175414
|
-
|
|
175415
|
-
|
|
175416
|
-
if (w === null || typeof w !== "object" || Array.isArray(w)) return { refusal: `${at2} is not an object; nothing was changed` };
|
|
175417
|
-
for (const [k, value] of Object.entries(w)) {
|
|
175418
|
-
if (!PAUSE_WINDOW_FIELDS.includes(k)) return { refusal: `${at2} has a field this file does not take (${PAUSE_WINDOW_FIELDS.join(", ")} only); nothing was changed` };
|
|
175419
|
-
const plain = typeof value === "string" || k === "days" && Array.isArray(value) && value.every((d) => typeof d === "string");
|
|
175420
|
-
if (!plain) return { refusal: `${at2}: ${k} must be ${k === "days" ? "a list of weekday names" : "a string"}; nothing was changed` };
|
|
175421
|
-
}
|
|
175422
|
-
}
|
|
175777
|
+
const shape2 = pauseListShapeRefusal(body.windows);
|
|
175778
|
+
if (shape2) return { refusal: `${shape2}; nothing was changed` };
|
|
175423
175779
|
const path6 = ctx.config.pauseWindowsPath;
|
|
175424
175780
|
const snap = readNow(ctx.fs, path6);
|
|
175425
175781
|
if (snap.refusal) return snap;
|
|
@@ -176097,6 +176453,8 @@ var init_actions = __esm({
|
|
|
176097
176453
|
envelopeAfter: "Set it back with the envelope form: the console kept no copy of the old values.",
|
|
176098
176454
|
tooBig: "Put the previous list back by hand: this preview showed it; the console keeps no copy.",
|
|
176099
176455
|
tooBigAfter: "Put the previous list back by hand: the console kept no copy of it.",
|
|
176456
|
+
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.",
|
|
176457
|
+
refusedListAfter: "Put the previous list back by hand: the console would refuse it as an Undo (a window it does not take, or more than 256 windows or days), and kept no copy of it.",
|
|
176100
176458
|
resumeUnread: "Pause the queues that were paused again by hand: the fleet was not read before the resume, so which they were is not known.",
|
|
176101
176459
|
resumeShort: "Pause the queues that were paused again by hand: not every queue was read before the resume, so which they were is not fully known."
|
|
176102
176460
|
});
|
|
@@ -178059,8 +178417,8 @@ var init_setup_wizard = __esm({
|
|
|
178059
178417
|
init_connection();
|
|
178060
178418
|
init_service_env();
|
|
178061
178419
|
init_index();
|
|
178062
|
-
RUNTIME_VERSION = "5.1
|
|
178063
|
-
RECEIVER_VERSION = "5.1
|
|
178420
|
+
RUNTIME_VERSION = "5.2.1";
|
|
178421
|
+
RECEIVER_VERSION = "5.2.1";
|
|
178064
178422
|
RUNTIME_PKG = "@edgehero/pi-dispatch";
|
|
178065
178423
|
RECEIVER_PKG = "@edgehero/pi-dispatch-receiver";
|
|
178066
178424
|
NUDGE_MARKER_BASENAME = "pi-dispatch-setup.nudged";
|
|
@@ -179940,7 +180298,7 @@ The project's runs could not be read (${sizing.unreachable}).`;
|
|
|
179940
180298
|
if (dim.held === "largest") return `no larger size (${dim.reason}: ${words})`;
|
|
179941
180299
|
return dim.reason === "not-enough-runs" ? notEnoughRunsText(dim, shown3 === formatCpus ? cpusText : shown3) : "fits";
|
|
179942
180300
|
};
|
|
179943
|
-
const facts7 = [s2.words.memoryFact, s2.words.cpuFact].filter(Boolean);
|
|
180301
|
+
const facts7 = [s2.words.memoryFact, s2.words.cpuFact, s2.words.memoryCeiling].filter(Boolean);
|
|
179944
180302
|
return `
|
|
179945
180303
|
Project ${id7}'s runs (the last ${SUGGEST_WINDOW_DAYS} days, measured inside the jobs, so advisory): size now ${formatMemory(m2.current)}, ${cpusText(c.current)}; ${peaks}. They suggest memory ${said(m2, s2.words.memory, formatMemory)}, CPUs ${said(c, s2.words.cpu, formatCpus)}.${facts7.length > 0 ? ` ${facts7.map((f3) => `${f3[0].toUpperCase()}${f3.slice(1)}.`).join(" ")}` : ""}`;
|
|
179946
180304
|
}
|