@codeyam-editor/codeyam-editor 0.1.5 → 0.1.6
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/npm/cli.js +5 -1
- package/npm/cloud.js +1884 -210
- package/npm/codesign-plan.js +68 -0
- package/npm/empty-project-dir.js +92 -7
- package/npm/fleet-add-failure.js +252 -0
- package/npm/fleet-asset-version.js +37 -0
- package/npm/fleet-auth-health.js +143 -5
- package/npm/fleet-build-data.js +60 -5
- package/npm/fleet-cleanup.js +114 -9
- package/npm/fleet-destroy-safety.js +230 -13
- package/npm/fleet-early-roster.js +87 -0
- package/npm/fleet-effect-poll.js +110 -0
- package/npm/fleet-env.js +86 -0
- package/npm/fleet-ghcr-secret.js +129 -0
- package/npm/fleet-git-maintenance.js +142 -0
- package/npm/fleet-host-op-limiter.js +68 -0
- package/npm/fleet-iap-token.js +165 -0
- package/npm/fleet-live-status.js +40 -8
- package/npm/fleet-memory.js +245 -9
- package/npm/fleet-plans.js +98 -4
- package/npm/fleet-pool.js +75 -16
- package/npm/fleet-prewarm.js +87 -11
- package/npm/fleet-project-drift.js +58 -0
- package/npm/fleet-public-url.js +234 -0
- package/npm/fleet-queue.js +58 -1
- package/npm/fleet-reachability-debounce.js +69 -0
- package/npm/fleet-reachability.js +80 -0
- package/npm/fleet-reset.js +32 -0
- package/npm/fleet-running-plan.js +85 -0
- package/npm/fleet-session-token-cache.js +65 -0
- package/npm/fleet-setup-steps.js +19 -12
- package/npm/fleet-status-refs.js +96 -0
- package/npm/fleet-strand.js +39 -8
- package/npm/fleet-stuck-status.js +13 -7
- package/npm/fleet-vm-access.js +170 -0
- package/npm/fleet-vm-probe-target.js +282 -0
- package/npm/fleet-vm-readiness.js +136 -3
- package/npm/google-ingress.js +15 -0
- package/npm/ingress-invoke.js +478 -0
- package/npm/launch-config.js +182 -6
- package/npm/scenario-check.js +282 -10
- package/npm/scenario-handlers.js +85 -27
- package/npm/scenario-interactivity.js +145 -6
- package/npm/scenario-issues.js +13 -1
- package/npm/scenario-mocks.js +129 -26
- package/npm/scenario-playwright.js +286 -12
- package/npm/script-drift.js +72 -1
- package/npm/utils.js +37 -3
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-D7VZkkaj.js → ContainerInspector-DgM0pTAx.js} +1 -1
- package/ui/dist/assets/EditorShell-B7Q06uJ_.js +252 -0
- package/ui/dist/assets/EditorShell-DP5UY20G.css +1 -0
- package/ui/dist/assets/{Markdown-BoLHHFXH.js → Markdown-Chgdf3PW.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-BRrEr8Ng.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-BeQfa28g.js +9 -0
- package/ui/dist/assets/{ScenarioDataPanel-DYP7pi_n.css → ScenarioDataPanel-C1LukYI6.css} +1 -1
- package/ui/dist/assets/{ScenarioDataPanelFullPage-CBLTGC4-.js → ScenarioDataPanelFullPage-BLqRCxQX.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CIAxfHPh.js +21 -0
- package/ui/dist/assets/{StepsSimulator-BFldWyoX.js → StepsSimulator-CymGgfbu.js} +1 -1
- package/ui/dist/assets/index-BygCiJSb.css +1 -0
- package/ui/dist/assets/{index-QsOiOG9x.js → index-DCHqRrwO.js} +4 -4
- package/ui/dist/assets/{markdown-v0F0UEt3.js → markdown-C9tbsVHX.js} +1 -1
- package/ui/dist/assets/react-nrLBr15I.js +28 -0
- package/ui/dist/assets/{useEvents-DHkLJZR4.js → useEvents-S3Qk1Y_d.js} +1 -1
- package/ui/dist/assets/useServerIdentity-B7rI0u3O.js +1 -0
- package/ui/dist/index.html +4 -4
- package/ui/dist/open-source/codeyam-counter.png +0 -0
- package/ui/dist/open-source/el-carot.png +0 -0
- package/ui/dist/open-source/tabcommand.png +0 -0
- package/ui/dist/assets/EditorShell-DHhIaotd.css +0 -1
- package/ui/dist/assets/EditorShell-r-796N_W.js +0 -191
- package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +0 -1
- package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +0 -5
- package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +0 -21
- package/ui/dist/assets/index-CIXZkwfB.css +0 -1
- package/ui/dist/assets/react-CSS0HapR.js +0 -28
package/npm/fleet-auth-health.js
CHANGED
|
@@ -8,37 +8,74 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Pure logic only — no I/O, no env reads, no clock.
|
|
10
10
|
|
|
11
|
+
const fr = require('./fleet-reachability');
|
|
12
|
+
|
|
11
13
|
// Classify operator gcloud auth health from the facts server.js gathers via the
|
|
12
|
-
// ${GCLOUD} chokepoint. Returns { degraded, reason }: degraded true means
|
|
13
|
-
// operator's own
|
|
14
|
-
// reset gate must suppress all reboots and the UI must surface a banner.
|
|
14
|
+
// ${GCLOUD} chokepoint. Returns { degraded, kind, reason }: degraded true means
|
|
15
|
+
// the operator's own side is faulted (which makes EVERY VM read unreachable), so
|
|
16
|
+
// the reset gate must suppress all reboots and the UI must surface a banner.
|
|
15
17
|
// account trimmed `gcloud config list account` output (may be empty)
|
|
16
18
|
// accountErrMsg non-null when the account-resolve exec itself errored
|
|
17
19
|
// tokenOk true/false when `auth print-access-token` was run; null when
|
|
18
20
|
// it was not (account-only phase) — a valid SA account with
|
|
19
21
|
// tokenOk null is "account looks fine, token not yet proven",
|
|
20
22
|
// which is NOT degraded, so the caller can proceed to the mint.
|
|
23
|
+
//
|
|
24
|
+
// `kind` DISCRIMINATES the two failures that used to collapse into one banner
|
|
25
|
+
// (2026-07-29): 'auth' means the CREDENTIAL is broken (re-bind the SA), while
|
|
26
|
+
// 'probe-failed' means the INVOCATION never produced an answer (restart the
|
|
27
|
+
// throttled process). Empty output with NO error message is the 'probe-failed'
|
|
28
|
+
// signature — a subprocess starved under cgroup throttling exits without writing
|
|
29
|
+
// anything, whereas a genuine credential failure always produces a message. Both
|
|
30
|
+
// still latch degraded: this adds a discriminator for the operator-facing
|
|
31
|
+
// RECOVERY, it does not weaken the guard. A 'probe-failed' verdict is
|
|
32
|
+
// inconclusive about credentials, so the caller must corroborate with local
|
|
33
|
+
// pressure signals before naming resource exhaustion — the mapping lives in
|
|
34
|
+
// fleet-destroy-safety.classifyDestroyBlock.
|
|
21
35
|
function classifyOperatorAuth({ account, accountErrMsg, tokenOk } = {}) {
|
|
22
36
|
const acct = typeof account === 'string' ? account.trim() : '';
|
|
23
37
|
if (accountErrMsg || !acct) {
|
|
24
38
|
return {
|
|
25
39
|
degraded: true,
|
|
40
|
+
kind: accountErrMsg ? 'auth' : 'probe-failed',
|
|
26
41
|
reason: `operator gcloud could not resolve an account (${accountErrMsg || 'empty output'})`,
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
if (!/\.gserviceaccount\.com$/.test(acct)) {
|
|
30
45
|
return {
|
|
31
46
|
degraded: true,
|
|
47
|
+
kind: 'auth',
|
|
32
48
|
reason: `operator gcloud is on a non-service-account '${acct}' — config drift; run scripts/operator/setup-service-account.sh`,
|
|
33
49
|
};
|
|
34
50
|
}
|
|
35
51
|
if (tokenOk === false) {
|
|
36
52
|
return {
|
|
37
53
|
degraded: true,
|
|
54
|
+
kind: 'auth',
|
|
38
55
|
reason: `operator gcloud cannot mint a token for ${acct} (expired/absent credentials); run scripts/operator/setup-service-account.sh`,
|
|
39
56
|
};
|
|
40
57
|
}
|
|
41
|
-
return { degraded: false, reason: null };
|
|
58
|
+
return { degraded: false, kind: null, reason: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Consecutive-failure debounce for the FLEET-WIDE operator-auth-degraded BANNER
|
|
62
|
+
// (vm--flap-tolerant-reachability). Splits the two behaviors of the degraded
|
|
63
|
+
// verdict: the auto-reset SUPPRESSION stays conservative (server.js suppresses a
|
|
64
|
+
// reboot on even ONE failed probe — a broken laptop must never reboot a healthy
|
|
65
|
+
// VM), but a single transient gcloud-resolve hiccup must NOT blank the whole
|
|
66
|
+
// fleet with the scary "every VM reads unreachable" banner (observed 2026-07-01:
|
|
67
|
+
// the exact command succeeded 5/5 right after; the degraded flag self-cleared).
|
|
68
|
+
// The BANNER / all-VMs-unreachable DISPLAY requires `threshold` CONSECUTIVE
|
|
69
|
+
// degraded probes; a single healthy probe resets the streak to 0.
|
|
70
|
+
// prevStreak the consecutive degraded-probe count from the previous check
|
|
71
|
+
// degraded this probe's raw degraded verdict (classifyOperatorAuth.degraded)
|
|
72
|
+
// threshold consecutive degraded probes required before the banner fires
|
|
73
|
+
// Returns { streak, display }: display true only once the streak crosses.
|
|
74
|
+
const OPERATOR_AUTH_DISPLAY_THRESHOLD = 3;
|
|
75
|
+
function debounceOperatorAuthDisplay(prevStreak, degraded, threshold = OPERATOR_AUTH_DISPLAY_THRESHOLD) {
|
|
76
|
+
if (!degraded) return { streak: 0, display: false };
|
|
77
|
+
const streak = (prevStreak || 0) + 1;
|
|
78
|
+
return { streak, display: streak >= threshold };
|
|
42
79
|
}
|
|
43
80
|
|
|
44
81
|
// True when at least `fleetWideThreshold` VMs are simultaneously past the
|
|
@@ -50,7 +87,11 @@ function classifyOperatorAuth({ account, accountErrMsg, tokenOk } = {}) {
|
|
|
50
87
|
function isFleetWideUnreachable(vms, threshold, fleetWideThreshold) {
|
|
51
88
|
let count = 0;
|
|
52
89
|
for (const vm of Array.isArray(vms) ? vms : []) {
|
|
53
|
-
|
|
90
|
+
// Only a DEFINITE unreachable verdict counts. A VM whose reachability is
|
|
91
|
+
// unknown (never polled, or held inconclusive) has established nothing, and
|
|
92
|
+
// an unknown must never be evidence in a decision that gates a reboot — the
|
|
93
|
+
// shared invariant in npm/fleet-reachability.js.
|
|
94
|
+
if (vm && fr.isConfirmedUnreachable(vm.reachable) && (vm.unreachableStreak || 0) >= threshold) count++;
|
|
54
95
|
}
|
|
55
96
|
return count >= fleetWideThreshold;
|
|
56
97
|
}
|
|
@@ -68,8 +109,105 @@ function classifyIapEditorProbe({ err, stdout } = {}) {
|
|
|
68
109
|
return code === '200';
|
|
69
110
|
}
|
|
70
111
|
|
|
112
|
+
// Choose WHICH signal decides a VM's per-poll reachability, and what it says.
|
|
113
|
+
//
|
|
114
|
+
// The laptop and the GCE operator host see a VM through different eyes. A laptop
|
|
115
|
+
// holds SSH `-L` forwards, so the cheap local-port probe (127.0.0.1:PORT_BASE+n)
|
|
116
|
+
// is a true reachability signal there. The GCE operator host has NO such
|
|
117
|
+
// forwards, so that probe fails for EVERY VM on every poll — which climbed each
|
|
118
|
+
// VM's unreachable streak into the vanish-reconcile path and fired a ~15-60s
|
|
119
|
+
// IAP-SSH tunnel probe per VM per cycle. With ~10 VMs those piled up faster than
|
|
120
|
+
// they drained and drove the host past load 25 (diagnosed live 2026-07-19).
|
|
121
|
+
//
|
|
122
|
+
// On GCE the fix is to read the LB editor backend-service health the dashboard
|
|
123
|
+
// ALREADY gathers (one cheap `gcloud ... backend-services get-health` per slow
|
|
124
|
+
// cycle) as the primary verdict: O(1 API call), no tunnel, fleet-scalable.
|
|
125
|
+
//
|
|
126
|
+
// onGce true on the GCE operator host (FLEET_ON_GCE)
|
|
127
|
+
// lbReachable the LB+IAP probe (probeVM) actually reached the VM's
|
|
128
|
+
// editor. Only supplied when PROBE_VIA_LB is on; a
|
|
129
|
+
// null/undefined means "no LB probe on this deployment"
|
|
130
|
+
// (the laptop) and this axis stays silent.
|
|
131
|
+
// backendEditorHealth classifyBackendHealth's verbatim verdict for the editor
|
|
132
|
+
// backend: 'healthy'|'unhealthy'|'unknown'|'absent'
|
|
133
|
+
// (null/undefined before the first slow gather lands)
|
|
134
|
+
// localProbeOk the 127.0.0.1:PORT_BASE+n editor probe answered
|
|
135
|
+
// iapVerdict classifyIapEditorProbe's result, when one is on hand
|
|
136
|
+
//
|
|
137
|
+
// Returns { reachable, source }:
|
|
138
|
+
// reachable true | false | null, where null is INCONCLUSIVE — hold. It must
|
|
139
|
+
// never read as reachable and must never advance an unreachable
|
|
140
|
+
// streak toward an auto-reset. A genuinely-down VM must not read
|
|
141
|
+
// healthy, and a not-yet-known VM must not read dead.
|
|
142
|
+
// source 'lb' | 'backend' | 'local' | 'iap' — which signal decided, for the log.
|
|
143
|
+
//
|
|
144
|
+
// Only 'absent' falls through to the local/IAP path on GCE: a VM with no
|
|
145
|
+
// vm-<N>-editor-bes has no LB signal at all (mid-add / un-wired), and there are
|
|
146
|
+
// few of those at any time, so the occasional IAP probe there is affordable.
|
|
147
|
+
// 'unknown' and the pre-first-gather null deliberately HOLD rather than fall
|
|
148
|
+
// through, because on GCE falling through means a guaranteed-false local probe.
|
|
149
|
+
function classifyReachability({ onGce, backendEditorHealth, localProbeOk, iapVerdict, lbReachable } = {}) {
|
|
150
|
+
if (onGce) {
|
|
151
|
+
// The LB+IAP probe outranks backend health (vm--gce-reachable-flag-from-lb-probe).
|
|
152
|
+
// It is the only signal on the GCE host that actually REACHES the VM's editor:
|
|
153
|
+
// the local-port probe has no SSH `-L` forward there so it always fails, and
|
|
154
|
+
// backendEditorHealth is filled by a slow gather that early-returns for
|
|
155
|
+
// not-yet-reachable VMs — the reachable⇄backend-health deadlock that pinned the
|
|
156
|
+
// dashboard at 0/10 while every probe was in fact succeeding. Only a definite
|
|
157
|
+
// boolean speaks; null/undefined (no LB probe configured — the laptop) falls
|
|
158
|
+
// through to the pre-existing backend/local/IAP ladder unchanged.
|
|
159
|
+
if (lbReachable === true) return { reachable: true, source: 'lb' };
|
|
160
|
+
if (lbReachable === false) return { reachable: false, source: 'lb' };
|
|
161
|
+
if (backendEditorHealth === 'healthy') return { reachable: true, source: 'backend' };
|
|
162
|
+
if (backendEditorHealth === 'unhealthy') return { reachable: false, source: 'backend' };
|
|
163
|
+
// 'unknown', or no gather yet (null/undefined): inconclusive, hold.
|
|
164
|
+
if (backendEditorHealth !== 'absent') return { reachable: null, source: 'backend' };
|
|
165
|
+
// 'absent' falls through to the laptop-shaped local/IAP path below.
|
|
166
|
+
}
|
|
167
|
+
// Laptop path (and GCE's backend-less VMs): the local port probe is primary,
|
|
168
|
+
// exactly as before. The IAP verdict only speaks when the local port is dark.
|
|
169
|
+
if (localProbeOk) return { reachable: true, source: 'local' };
|
|
170
|
+
if (iapVerdict === true) return { reachable: true, source: 'iap' };
|
|
171
|
+
if (iapVerdict === false) return { reachable: false, source: 'iap' };
|
|
172
|
+
return { reachable: false, source: 'local' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Resolve ONE poll's reachability record from a (possibly inconclusive) verdict
|
|
176
|
+
// plus the prior poll's state. Where classifyReachability answers "which signal
|
|
177
|
+
// decides, and what does it say", this answers "so what does this poll record".
|
|
178
|
+
//
|
|
179
|
+
// This is the safety-critical half of vm--lb-health-reachability-on-gce: an
|
|
180
|
+
// INCONCLUSIVE verdict (reachable null — on GCE, backend health 'unknown' or not
|
|
181
|
+
// yet gathered) must neither invent a reachability nor advance the unreachable
|
|
182
|
+
// streak. The streak is what pollVanishedInstances gates on before it spends an
|
|
183
|
+
// IAP probe and ultimately reboots a VM, so letting an unknown signal increment
|
|
184
|
+
// it would march a VM toward a destructive reset on evidence we do not have.
|
|
185
|
+
// Instead: hold the last-known reachability, and FREEZE the streak.
|
|
186
|
+
//
|
|
187
|
+
// verdict classifyReachability's { reachable, source }
|
|
188
|
+
// prevReachable the previous poll's resolved reachability
|
|
189
|
+
// prevUnreachableStreak the previous poll's consecutive-unreachable count
|
|
190
|
+
//
|
|
191
|
+
// Returns { reachable, unreachableStreak }. A reachable poll zeroes the streak
|
|
192
|
+
// immediately (we never delay clearing a false alarm, only delay raising one).
|
|
193
|
+
function resolveReachabilityPoll({ verdict, prevReachable, prevUnreachableStreak } = {}) {
|
|
194
|
+
const prevStreak = prevUnreachableStreak || 0;
|
|
195
|
+
const raw = verdict ? verdict.reachable : null;
|
|
196
|
+
if (raw === null || raw === undefined) {
|
|
197
|
+
return { reachable: !!prevReachable, unreachableStreak: prevStreak };
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
reachable: !!raw,
|
|
201
|
+
unreachableStreak: raw ? 0 : prevStreak + 1,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
71
205
|
module.exports = {
|
|
72
206
|
classifyOperatorAuth,
|
|
207
|
+
debounceOperatorAuthDisplay,
|
|
208
|
+
OPERATOR_AUTH_DISPLAY_THRESHOLD,
|
|
73
209
|
isFleetWideUnreachable,
|
|
74
210
|
classifyIapEditorProbe,
|
|
211
|
+
classifyReachability,
|
|
212
|
+
resolveReachabilityPoll,
|
|
75
213
|
};
|
package/npm/fleet-build-data.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fr = require('./fleet-reachability');
|
|
4
|
+
|
|
3
5
|
// Assemble the per-VM wire object the dashboard's /data response carries to the
|
|
4
6
|
// browser. This is the single source of truth for a card's transport shape: the
|
|
5
7
|
// card UI (scripts/operator/fleet-dashboard/public/index.html) reads `v.<field>`
|
|
@@ -21,7 +23,37 @@ function buildVmEnvelope(v, ctx) {
|
|
|
21
23
|
return {
|
|
22
24
|
n: ctx.n,
|
|
23
25
|
url: v.url || null,
|
|
24
|
-
|
|
26
|
+
// The VM's public Live Preview URL (<slug>.preview.<domain> in named-tunnel
|
|
27
|
+
// mode, else the local forward). The card reads it as `previewSub` to render
|
|
28
|
+
// the 👁 preview link. Server-computed in server.js and merged onto the poll
|
|
29
|
+
// state — but it was dropped here, so the link was silently absent on EVERY
|
|
30
|
+
// card fleet-wide even when the VM's preview served fine. The exact
|
|
31
|
+
// currentProvider/vmAccess envelope-drift class this module guards against.
|
|
32
|
+
previewUrl: v.previewUrl || null,
|
|
33
|
+
// The app dev-server port the VM serves on (Vite 5173, Next 3000, …),
|
|
34
|
+
// resolved stickily upstream. The card reads it as `appUp` to gate the
|
|
35
|
+
// preview link live. Dropped alongside previewUrl — same silent-omit bug.
|
|
36
|
+
appPort: v.appPort != null ? v.appPort : null,
|
|
37
|
+
// Sticky preview-SERVING signal — "is the VM's dev server up?" — derived
|
|
38
|
+
// from the previewState the editor emits on /api/session-info (forwarding →
|
|
39
|
+
// true, placeholder → false, else null). Reaches every VM over the IAP-LB
|
|
40
|
+
// seam on the GCE operator host, where the old loopback `previewReachable`
|
|
41
|
+
// read false fleet-wide. The card gates the clickable 👁 link on `=== true`.
|
|
42
|
+
// Three-state, never conflating "down" with "unknown".
|
|
43
|
+
previewServing: v.previewServing === true ? true : (v.previewServing === false ? false : null),
|
|
44
|
+
// Sticky preview-INGRESS signal — "is the VM's public preview URL wired?" —
|
|
45
|
+
// from a reachability probe of <slug>.preview.<domain> over the LB, DISTINCT
|
|
46
|
+
// from previewServing. true = wired, false = confirmed dead (→ muted 'public
|
|
47
|
+
// URL not wired' hint), null = unknown (skipped / IAP audience mismatch — a
|
|
48
|
+
// null must never gate the link off). Three-state, same discipline.
|
|
49
|
+
previewIngress: v.previewIngress === true ? true : (v.previewIngress === false ? false : null),
|
|
50
|
+
// `reachable` (three-state) + `reachableSource` (which signal decided, or
|
|
51
|
+
// 'unpolled'). Same tri-state discipline as previewServing/previewIngress
|
|
52
|
+
// above, but delegated to npm/fleet-reachability.js because FIVE consumers
|
|
53
|
+
// ask the same question and the old `!!v.reachable` coercion here is what
|
|
54
|
+
// manufactured the 2026-07-30 fleet-wide phantom outage — 13 cards claiming
|
|
55
|
+
// a definite `false` that no probe had ever established.
|
|
56
|
+
...fr.normalizeReachability(v),
|
|
25
57
|
hasSession: !!v.hasSession,
|
|
26
58
|
// Three-state: true = broker confirmed up, false = confirmed down, null =
|
|
27
59
|
// editor unreachable so broker state is unknown. Never conflate "down" with
|
|
@@ -41,6 +73,14 @@ function buildVmEnvelope(v, ctx) {
|
|
|
41
73
|
// instead of active-green 'building'.
|
|
42
74
|
needsCleanup: !!v.needsCleanup,
|
|
43
75
|
feature: v.feature || null,
|
|
76
|
+
// The VM's live-claimed plan slug, reported by /api/session-info and merged
|
|
77
|
+
// (sticky) onto the poll state. The card renders it as the what-it's-working-on
|
|
78
|
+
// line when `feature` is null — a VM that has claimed a plan but not yet named
|
|
79
|
+
// a feature (early plan/explore steps) previously showed a blank "no feature
|
|
80
|
+
// (idle)" card. Preferred over the origin claim-ref because this live source
|
|
81
|
+
// never goes stale under a failed claim-ref push. null for a legacy editor
|
|
82
|
+
// that predates the field, or a VM with no claimed plan.
|
|
83
|
+
selectedPlan: v.selectedPlan || null,
|
|
44
84
|
step: v.step != null ? v.step : null,
|
|
45
85
|
mode: v.mode || null,
|
|
46
86
|
label: v.label || null,
|
|
@@ -72,6 +112,14 @@ function buildVmEnvelope(v, ctx) {
|
|
|
72
112
|
// Auth state + reason + token expiry. Only surfaced when the editor is
|
|
73
113
|
// reachable (an unreachable VM's cached auth is stale and misleading).
|
|
74
114
|
providerAuth: v.reachable && v.providerAuth ? v.providerAuth : null,
|
|
115
|
+
// The silently-broken Live Preview guard: { previewOrigin, expectedOrigin }
|
|
116
|
+
// when the VM's own /api/config proves its preview can never load in a
|
|
117
|
+
// remote browser (a not-same-origin-safe stack stuck on a loopback/subpath
|
|
118
|
+
// origin), null when healthy or unknown. Drives the card's amber "Live
|
|
119
|
+
// Preview unreachable" badge. Gated on `reachable` like providerAuth — an
|
|
120
|
+
// unreachable VM's last-known config is stale, and an unreachable card has
|
|
121
|
+
// louder problems to report than its preview origin.
|
|
122
|
+
previewMisconfigured: (v.reachable && v.previewMisconfigured) || null,
|
|
75
123
|
effects: v.effects || null,
|
|
76
124
|
// The VM's live-configured build agent (lowercase provider), reported by
|
|
77
125
|
// /api/session-info and merged (sticky) onto the poll state. Drives the
|
|
@@ -107,6 +155,13 @@ function buildVmEnvelope(v, ctx) {
|
|
|
107
155
|
// against — so the line and list rendered empty even when the sidecar held
|
|
108
156
|
// principals. Defaults to [] for a VM with no list.
|
|
109
157
|
vmAccess: Array.isArray(v.vmAccess) ? v.vmAccess : [],
|
|
158
|
+
// Per-slot persisted-env override verdict (set by reapplyVmEnv, sticky on
|
|
159
|
+
// poll state): { present, skipped, reason, taggedRepo, setAt, keys }. Drives
|
|
160
|
+
// the card's env-override badge — an "applied" chip listing the masked keys +
|
|
161
|
+
// tagged repo, or a warning when a stale/untagged override is NOT replayed so
|
|
162
|
+
// a months-old override is never invisible again. null when no env is
|
|
163
|
+
// persisted for the slot (or the slot has never been reapplied).
|
|
164
|
+
envOverride: v.envOverride || null,
|
|
110
165
|
};
|
|
111
166
|
}
|
|
112
167
|
|
|
@@ -117,14 +172,14 @@ function buildVmEnvelope(v, ctx) {
|
|
|
117
172
|
// card renders means adding it here AND in buildVmEnvelope — the test fails loud
|
|
118
173
|
// if you do one without the other.
|
|
119
174
|
const REQUIRED_CARD_FIELDS = [
|
|
120
|
-
'n', 'url', 'reachable', 'hasSession', 'brokerLive', 'brokerStatus',
|
|
121
|
-
'awaitingInput', 'needsCleanup', 'feature', 'step', 'mode', 'label', 'status', 'orphan',
|
|
175
|
+
'n', 'url', 'previewUrl', 'appPort', 'previewServing', 'previewIngress', 'reachable', 'reachableSource', 'hasSession', 'brokerLive', 'brokerStatus',
|
|
176
|
+
'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'status', 'orphan',
|
|
122
177
|
'degradedReasons', 'setupSteps', 'attentionReasons',
|
|
123
178
|
'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen',
|
|
124
179
|
'job', 'config', 'project', 'variant', 'cardIdentity', 'brokerHistory',
|
|
125
|
-
'brokerDeath', 'providerAuth', 'effects', 'currentProvider',
|
|
180
|
+
'brokerDeath', 'providerAuth', 'previewMisconfigured', 'effects', 'currentProvider',
|
|
126
181
|
'readiness', 'backendHealth', 'vmAccess',
|
|
127
|
-
'startedAt', 'featureStartedAt', 'attention',
|
|
182
|
+
'startedAt', 'featureStartedAt', 'attention', 'envOverride',
|
|
128
183
|
];
|
|
129
184
|
|
|
130
185
|
module.exports = { buildVmEnvelope, REQUIRED_CARD_FIELDS };
|
package/npm/fleet-cleanup.js
CHANGED
|
@@ -78,13 +78,19 @@ function classifyDescribeError(err, stderr) {
|
|
|
78
78
|
// Decide what the startup reap should do for one sidecar. Pure: takes the
|
|
79
79
|
// decoded sidecar + GCP-existence verdict + (for adds) container-up verdict +
|
|
80
80
|
// (for up adds) project-wiring verdict, returns a tag the caller dispatches on.
|
|
81
|
-
// verdict
|
|
82
|
-
// 'cleanup'
|
|
83
|
-
// 'reconcile-add'
|
|
84
|
-
// 'half-provisioned'
|
|
85
|
-
// '
|
|
86
|
-
//
|
|
87
|
-
// '
|
|
81
|
+
// verdict when dispatch
|
|
82
|
+
// 'cleanup' destroy gone, OR add gone runDestroyCleanup / runAddCleanup
|
|
83
|
+
// 'reconcile-add' add up, container up, project wired, agent usable ensure roster + recordLaunch, then clear
|
|
84
|
+
// 'half-provisioned' add up, container up, but project NOT wired surface needs-recovery card (finish-wire from cfg)
|
|
85
|
+
// 'provision-incomplete' add up + container up + wired, but agent NOT surface needs-recovery card (the agent
|
|
86
|
+
// usable (CLI missing / auth wedged at unknown) isn't installed/authed — VM5 false-green)
|
|
87
|
+
// 'recover-bootstrap' add instance up but container missing/down re-run cloud:up to finish the half-done bootstrap
|
|
88
|
+
// (runAddRecoverBootstrap first probes the editor
|
|
89
|
+
// directly and diverts to rostering when the
|
|
90
|
+
// bootstrap is actually done and only the LB
|
|
91
|
+
// ingress is missing)
|
|
92
|
+
// 'clear-only' destroy sidecar but instance still exists just clear the sidecar
|
|
93
|
+
// 'leave' gcloud uncertain (exists === null) leave for the next boot
|
|
88
94
|
// The (exists, containerUp) split distinguishes a finished add from a container-
|
|
89
95
|
// down half-provisioned one: a dashboard SIGKILL'd mid-`cloud:up` leaves a GCP
|
|
90
96
|
// instance with no docker/container, which the old (exists-only) matrix wrongly
|
|
@@ -99,17 +105,82 @@ function classifyDescribeError(err, stderr) {
|
|
|
99
105
|
// for an add whose instance exists; destroy and the instance-gone add path ignore
|
|
100
106
|
// them. Splitting the matrix out lets the table be exhaustively tested without any
|
|
101
107
|
// of the side-effects in server.js.
|
|
102
|
-
|
|
108
|
+
//
|
|
109
|
+
// The `agentReady` axis (vm: Readiness Verifies Real Usability) closes the VM5
|
|
110
|
+
// false-green: an instance + container that ARE up and wired, but whose build
|
|
111
|
+
// agent isn't usable — the CLI isn't installed, or the auth probe is wedged at
|
|
112
|
+
// `unknown` — is NOT a finished add. Mirrors `projectWired`'s posture EXACTLY:
|
|
113
|
+
// only a POSITIVE not-ready signal (`agentReady === false`) downgrades, to
|
|
114
|
+
// 'provision-incomplete'; `null`/`undefined` (the probe couldn't decide, or an
|
|
115
|
+
// older editor doesn't report it) stays 'reconcile-add' so a flaky probe never
|
|
116
|
+
// strands a healthy VM. Checked AFTER `projectWired` — an unwired box is the more
|
|
117
|
+
// fundamental fault and keeps its own verdict.
|
|
118
|
+
//
|
|
119
|
+
// NOT an axis here (vm--batch-add-must-roster-and-wire-each-vm): "the editor
|
|
120
|
+
// answers on its own port but has no LB ingress". That state is a finished
|
|
121
|
+
// bootstrap missing its wiring, and re-running cloud:up cannot create the
|
|
122
|
+
// missing ingress — so it used to burn the 3-attempt budget and land on
|
|
123
|
+
// 'bootstrap-recovery-exhausted', because the signal being retried on
|
|
124
|
+
// (`lbReachable`) can never pass while the ingress is absent:
|
|
125
|
+
//
|
|
126
|
+
// no ingress → LB 503 → not reachable → recover → still no ingress → exhausted
|
|
127
|
+
//
|
|
128
|
+
// The rescue deliberately lives in server.js's runAddRecoverBootstrap rather
|
|
129
|
+
// than as a verdict here, because it needs a live IAP probe and is only worth
|
|
130
|
+
// paying for on the branch that was about to spend a recovery attempt. Adding it
|
|
131
|
+
// as a `reapVerdict` axis would force every container-down reap to run that
|
|
132
|
+
// probe eagerly just to compute a verdict, for a state that is rare.
|
|
133
|
+
function reapVerdict({ action, exists, containerUp, projectWired, agentReady, lbReachable }) {
|
|
103
134
|
if (exists === null) return 'leave';
|
|
104
135
|
if (action === 'destroy') return exists ? 'clear-only' : 'cleanup';
|
|
105
136
|
if (action === 'add') {
|
|
106
137
|
if (!exists) return 'cleanup';
|
|
138
|
+
// The `lbReachable` axis (vm--gce-reachable-flag-from-lb-probe) closes the
|
|
139
|
+
// false-orphan: a VM whose editor ANSWERS over the LB is provisioned by
|
|
140
|
+
// definition — its add finished, whatever a stale `add-<n>.json` sidecar
|
|
141
|
+
// left lying around implies. Without this, the container probe below could
|
|
142
|
+
// report "container down" for a VM that is demonstrably up and serving,
|
|
143
|
+
// driving recover-bootstrap → "exceeded 3 attempts" → a
|
|
144
|
+
// needs-manual-recovery card for a perfectly healthy box.
|
|
145
|
+
// Inverted posture vs projectWired/agentReady, and deliberately so: those
|
|
146
|
+
// axes let only a NEGATIVE signal downgrade, this one lets only a POSITIVE
|
|
147
|
+
// signal promote. `false`/`null`/`undefined` (probe said no, couldn't
|
|
148
|
+
// decide, or never ran) all fall through to the unchanged ladder below, so
|
|
149
|
+
// this can never strand a VM — it can only rescue one.
|
|
150
|
+
if (lbReachable === true) return 'reconcile-add';
|
|
107
151
|
if (!containerUp) return 'recover-bootstrap';
|
|
108
|
-
|
|
152
|
+
if (projectWired === false) return 'half-provisioned';
|
|
153
|
+
if (agentReady === false) return 'provision-incomplete';
|
|
154
|
+
return 'reconcile-add';
|
|
109
155
|
}
|
|
110
156
|
return 'leave';
|
|
111
157
|
}
|
|
112
158
|
|
|
159
|
+
// Classify the startup reap's LB rescue signal from a best-effort LB+IAP editor
|
|
160
|
+
// probe. Pure: server.js runs the HTTP probe and hands the raw outcome here, the
|
|
161
|
+
// same split classifyProjectWiring follows, so the rule is unit-testable without
|
|
162
|
+
// a live VM or an LB.
|
|
163
|
+
//
|
|
164
|
+
// Deliberately tri-state and ASYMMETRIC — it returns `true` or `null`, NEVER
|
|
165
|
+
// `false`:
|
|
166
|
+
// - probeViaLb false (a laptop has no LB to ask) → null (no opinion)
|
|
167
|
+
// - err present (threw, auth fault, timeout) → null (no opinion)
|
|
168
|
+
// - the editor answered → true (add is finished)
|
|
169
|
+
// - the editor did not answer → null (NOT false)
|
|
170
|
+
//
|
|
171
|
+
// That last line is the load-bearing one. A silent probe is not evidence a VM is
|
|
172
|
+
// dead — the LB may be unwired mid-add, or the IAP token not yet minted. Since
|
|
173
|
+
// reapVerdict promotes on `true` and ignores everything else, returning null
|
|
174
|
+
// leaves the pre-existing container/wiring ladder in charge. Returning `false`
|
|
175
|
+
// here would be indistinguishable to reapVerdict, but it would be a lie about
|
|
176
|
+
// what the probe proved, and the next person to add an axis would read it as
|
|
177
|
+
// evidence. The invariant this pins: this signal can rescue a VM, never strand one.
|
|
178
|
+
function classifyLbReachableForReap({ probeViaLb, reachable, err } = {}) {
|
|
179
|
+
if (!probeViaLb) return null;
|
|
180
|
+
if (err) return null;
|
|
181
|
+
return reachable ? true : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
113
184
|
// Classify whether a VM's editor `/workspace` is actually wired to a client
|
|
114
185
|
// project, from a best-effort probe of two signals: the git remote URL and
|
|
115
186
|
// whether `package.json` carries a real `name`. Pure: server.js runs the SSH +
|
|
@@ -414,6 +485,38 @@ function reconcileRosteredVm({ exists, hasSession, hasPendingAdd, streak, resetH
|
|
|
414
485
|
return "keep";
|
|
415
486
|
}
|
|
416
487
|
|
|
488
|
+
// The bootstrap-recovery ladder step the startup reap runs for a VM whose first
|
|
489
|
+
// cloud:up did not finish: decide (via recoverBootstrapDecision) whether another
|
|
490
|
+
// attempt is allowed, then either flag the VM exhausted (leave the sidecar for
|
|
491
|
+
// manual action) or clear any stale orphan flag and re-run cloud:up via `addVm`.
|
|
492
|
+
// The dashboard's `state` and `addVm` are injected so this stays a pure,
|
|
493
|
+
// importable unit — `log`/`err` default to the console but are overridable in
|
|
494
|
+
// tests. Behavior mirrors the previous inline server.js definition exactly.
|
|
495
|
+
function continueBootstrapRecovery(
|
|
496
|
+
n,
|
|
497
|
+
meta,
|
|
498
|
+
cfg,
|
|
499
|
+
{ state, addVm, log = console.log, err = console.error } = {},
|
|
500
|
+
) {
|
|
501
|
+
const { attempt, proceed, max } = recoverBootstrapDecision({
|
|
502
|
+
recoverAttempts: meta.recoverAttempts,
|
|
503
|
+
});
|
|
504
|
+
if (!proceed) {
|
|
505
|
+
err(
|
|
506
|
+
`startup-reap: VM-${n} exceeded ${max} bootstrap-recovery attempts — leaving sidecar for manual action`,
|
|
507
|
+
);
|
|
508
|
+
state.orphans[n] = { reason: "bootstrap-recovery-exhausted", attempts: attempt - 1 };
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
log(
|
|
512
|
+
`startup-reap: VM-${n} re-running cloud:up to finish bootstrap (recovery attempt ${attempt}/${max})`,
|
|
513
|
+
);
|
|
514
|
+
// A prior exhausted/no-cfg run may have flagged this VM; clear it now that a
|
|
515
|
+
// fresh attempt is starting so the card moves to `recovering`, not stale red.
|
|
516
|
+
delete state.orphans[n];
|
|
517
|
+
addVm(n, cfg, { recovering: true, recoverAttempt: attempt });
|
|
518
|
+
}
|
|
519
|
+
|
|
417
520
|
module.exports = {
|
|
418
521
|
parsePendingJobFilename,
|
|
419
522
|
persistPendingJob,
|
|
@@ -424,7 +527,9 @@ module.exports = {
|
|
|
424
527
|
reconcileRosteredVm,
|
|
425
528
|
orphanedAddsToReconcile,
|
|
426
529
|
classifyProjectWiring,
|
|
530
|
+
classifyLbReachableForReap,
|
|
427
531
|
recoverBootstrapDecision,
|
|
532
|
+
continueBootstrapRecovery,
|
|
428
533
|
exitMarkerName,
|
|
429
534
|
parseExitMarker,
|
|
430
535
|
readExitMarker,
|