@codeyam-editor/codeyam-editor 0.1.13-staging.g1b6c798 → 0.1.13
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/app-port-drift.js +169 -0
- package/npm/cloud.js +208 -22
- package/npm/ingress-invoke.js +11 -2
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-nU0qr8np.js → ContainerInspector-DKH4LjcY.js} +1 -1
- package/ui/dist/assets/{EditorShell-Nzjasb6I.js → EditorShell-BwNMzsnF.js} +66 -65
- package/ui/dist/assets/{ScenarioDataPanel-BEslyDRr.js → ScenarioDataPanel-CtYXcbIr.js} +10 -10
- package/ui/dist/assets/{ScenarioDataPanelFullPage-DdKVvDwC.js → ScenarioDataPanelFullPage-DrhY8Okl.js} +1 -1
- package/ui/dist/assets/{ScenarioRenderer-6CcF5jhy.js → ScenarioRenderer-kBIhG1qo.js} +3 -3
- package/ui/dist/assets/index-4TZsSjHA.css +1 -0
- package/ui/dist/assets/{index-C9TVsqen.js → index-mgNPDFlH.js} +2 -2
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-D67Yd3Bo.css +0 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Detect a fleet VM whose app port drifted away from the port its container
|
|
4
|
+
// publishes on the host.
|
|
5
|
+
//
|
|
6
|
+
// A VM's compose render publishes the app port resolved ONCE at bootstrap
|
|
7
|
+
// (`apps[0].port ?? port ?? 5173`). When the project later moves its app port
|
|
8
|
+
// in-repo, the editor's preview proxy follows the new port while the host keeps
|
|
9
|
+
// publishing the old one: docker-proxy accepts the load balancer's connection
|
|
10
|
+
// and immediately resets it, and every in-container probe reads green because
|
|
11
|
+
// the editor probes its own proxy on the new port. The only comparison that can
|
|
12
|
+
// see it is DECLARED (editor.json + editor.local.json) against PUBLISHED (what
|
|
13
|
+
// the running container actually maps on the host) — that is what this module
|
|
14
|
+
// does. It never repairs: changing the publication means recreating the
|
|
15
|
+
// container, which drops every live agent session.
|
|
16
|
+
//
|
|
17
|
+
// Pure helpers plus an injectable collector, so the reconcile and `vm-ingress.sh
|
|
18
|
+
// status` share one rule and the unit tests never reach for SSH.
|
|
19
|
+
|
|
20
|
+
/** The editor's control port. Pinned by contract, so on a VM's editor container
|
|
21
|
+
* the app surface is the OTHER published mapping — the same disambiguation
|
|
22
|
+
* `rcc_recover_ports` (scripts/lib/render-cloud-compose.sh) makes. */
|
|
23
|
+
const EDITOR_CONTROL_PORT = 14199;
|
|
24
|
+
|
|
25
|
+
const PORT_DRIFT_RUNBOOK = "docs/preview-port-drift.md";
|
|
26
|
+
|
|
27
|
+
function isValidPort(n) {
|
|
28
|
+
return Number.isInteger(n) && n > 0 && n < 65536;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Remote command that prints the editor container's published port list
|
|
32
|
+
* (`docker ps` `{{.Ports}}`), selecting the container by the control port it
|
|
33
|
+
* publishes rather than by a name, so a client VM whose compose project is not
|
|
34
|
+
* named `codeyam-editor` resolves the same way. Reads the LIVE bindings — no
|
|
35
|
+
* compose-file path to guess. Pure string builder. */
|
|
36
|
+
function buildPublishedAppPortReadCommand() {
|
|
37
|
+
return `docker ps --filter publish=${EDITOR_CONTROL_PORT} --format '{{.Ports}}' 2>/dev/null`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse that command's output into the published app port, or null.
|
|
41
|
+
*
|
|
42
|
+
* `{{.Ports}}` reads like `0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp,
|
|
43
|
+
* 10.0.0.5:14199->14199/tcp`. The app port is the single host port that is not
|
|
44
|
+
* the control port. Anything else — no container, more than one container, no
|
|
45
|
+
* control-port mapping, zero or several candidate app ports — returns null:
|
|
46
|
+
* an unreadable publication must never be reported as a mismatch. */
|
|
47
|
+
function parsePublishedAppPort(stdout) {
|
|
48
|
+
if (typeof stdout !== "string") return null;
|
|
49
|
+
const lines = stdout
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((l) => l.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
if (lines.length !== 1) return null;
|
|
54
|
+
const hostPorts = new Set();
|
|
55
|
+
for (const m of lines[0].matchAll(/:(\d+)->\d+\/tcp/g)) hostPorts.add(Number(m[1]));
|
|
56
|
+
if (!hostPorts.delete(EDITOR_CONTROL_PORT)) return null;
|
|
57
|
+
if (hostPorts.size !== 1) return null;
|
|
58
|
+
const [port] = hostPorts;
|
|
59
|
+
return isValidPort(port) ? port : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Parse `cloud-session-idle.sh` stdout into `idle` | `active` | null. Null
|
|
63
|
+
* (unknown) is deliberately NOT idle — the same asymmetry that script and
|
|
64
|
+
* `converge-vm.sh --converge-env` honour. */
|
|
65
|
+
function parseIdleVerdict(stdout) {
|
|
66
|
+
if (typeof stdout !== "string") return null;
|
|
67
|
+
const v = stdout.trim().split("\n").pop();
|
|
68
|
+
return v === "idle" || v === "active" ? v : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The remediations that apply to one drifted VM, cheapest first.
|
|
72
|
+
*
|
|
73
|
+
* The session-preserving pin always applies. The re-publish (recreate the
|
|
74
|
+
* container so the host publishes the declared port) is named ONLY when the
|
|
75
|
+
* VM is known idle, because it drops the PTY broker and every live session. */
|
|
76
|
+
function appPortDriftRemediations({ vmNumber, declared, published, idle }) {
|
|
77
|
+
const out = [
|
|
78
|
+
{
|
|
79
|
+
kind: "pin-to-published",
|
|
80
|
+
preservesSessions: true,
|
|
81
|
+
text:
|
|
82
|
+
`on VM-${vmNumber}, run \`codeyam-editor editor config-override apps.0.port ${published}\` ` +
|
|
83
|
+
`in the project — pins the app back to the published port with an in-place proxy rebind, no restart`,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
if (idle === "idle") {
|
|
87
|
+
out.push({
|
|
88
|
+
kind: "republish-declared",
|
|
89
|
+
preservesSessions: false,
|
|
90
|
+
text:
|
|
91
|
+
`VM-${vmNumber} is idle, so the container can instead be recreated to publish ${declared}: ` +
|
|
92
|
+
`change the app-port mapping in its docker-compose.cloud.yml from ${published} to ${declared} ` +
|
|
93
|
+
`and recreate the editor container (see ${PORT_DRIFT_RUNBOOK})`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compare one VM's declared app port against its published host port.
|
|
100
|
+
*
|
|
101
|
+
* `deps.readDeclaredPort` / `deps.readPublishedPort` return a port or null;
|
|
102
|
+
* `deps.readIdleVerdict` (optional) returns `idle` | `active` | null and is
|
|
103
|
+
* only consulted once a drift is found. Returns `[]` when the two agree or
|
|
104
|
+
* when EITHER cannot be read — no 5173 fallback is ever used for the
|
|
105
|
+
* comparison — else a one-element findings array. The published read is
|
|
106
|
+
* skipped when the declared port is unreadable, so a VM with no scaffolded
|
|
107
|
+
* project costs one probe, not two. */
|
|
108
|
+
async function collectAppPortDrift(vm, deps) {
|
|
109
|
+
const { vmNumber, slug = null } = vm || {};
|
|
110
|
+
const declared = await deps.readDeclaredPort();
|
|
111
|
+
if (!isValidPort(declared)) return [];
|
|
112
|
+
const published = await deps.readPublishedPort();
|
|
113
|
+
if (!isValidPort(published) || published === declared) return [];
|
|
114
|
+
const idle = deps.readIdleVerdict ? await deps.readIdleVerdict() : null;
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
vmNumber,
|
|
118
|
+
slug,
|
|
119
|
+
declared,
|
|
120
|
+
published,
|
|
121
|
+
idle,
|
|
122
|
+
remediations: appPortDriftRemediations({ vmNumber, declared, published, idle }),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Render one finding as the operator-facing warning line(s). */
|
|
128
|
+
function formatAppPortDriftWarning(finding) {
|
|
129
|
+
const label = finding.slug || `vm-${finding.vmNumber}`;
|
|
130
|
+
const lines = [
|
|
131
|
+
` VM-${finding.vmNumber} ${label}: PORT DRIFT — the project declares app port ` +
|
|
132
|
+
`${finding.declared} but the container publishes ${finding.published}, so the ` +
|
|
133
|
+
`load balancer cannot reach the Live Preview (in-container probes still read healthy).`,
|
|
134
|
+
];
|
|
135
|
+
for (const r of finding.remediations) lines.push(` fix: ${r.text}`);
|
|
136
|
+
if (finding.idle !== "idle") {
|
|
137
|
+
lines.push(
|
|
138
|
+
` (recreating the container to publish ${finding.declared} is not offered: ` +
|
|
139
|
+
`the VM is ${finding.idle || "not confirmed idle"} and a recreate drops live sessions)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Render the single machine-readable field `vm-ingress.sh status` prints.
|
|
146
|
+
* Kept separate from the `verdict=` line on purpose: the verdict drives the
|
|
147
|
+
* reconcile's re-`up` loop, and re-`up`ping cannot fix a port-publication
|
|
148
|
+
* drift. */
|
|
149
|
+
function formatPortDriftStatusLine(findings, { declared = null, published = null } = {}) {
|
|
150
|
+
if (findings.length) {
|
|
151
|
+
const f = findings[0];
|
|
152
|
+
return `port-drift=yes declared=${f.declared} published=${f.published}`;
|
|
153
|
+
}
|
|
154
|
+
if (isValidPort(declared) && isValidPort(published)) {
|
|
155
|
+
return `port-drift=no declared=${declared} published=${published}`;
|
|
156
|
+
}
|
|
157
|
+
return "port-drift=unknown";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
EDITOR_CONTROL_PORT,
|
|
162
|
+
buildPublishedAppPortReadCommand,
|
|
163
|
+
parsePublishedAppPort,
|
|
164
|
+
parseIdleVerdict,
|
|
165
|
+
appPortDriftRemediations,
|
|
166
|
+
collectAppPortDrift,
|
|
167
|
+
formatAppPortDriftWarning,
|
|
168
|
+
formatPortDriftStatusLine,
|
|
169
|
+
};
|
package/npm/cloud.js
CHANGED
|
@@ -105,6 +105,17 @@ const {
|
|
|
105
105
|
firewallNeedsConverge,
|
|
106
106
|
firewallAdmitsPort,
|
|
107
107
|
} = require("./ingress-invoke");
|
|
108
|
+
// Declared-vs-published app-port comparison (pure; unit-tested in
|
|
109
|
+
// app-port-drift.test.js). The SSH readers that feed it live below, beside
|
|
110
|
+
// readVmAppPort.
|
|
111
|
+
const {
|
|
112
|
+
buildPublishedAppPortReadCommand,
|
|
113
|
+
parsePublishedAppPort,
|
|
114
|
+
parseIdleVerdict,
|
|
115
|
+
collectAppPortDrift,
|
|
116
|
+
formatAppPortDriftWarning,
|
|
117
|
+
formatPortDriftStatusLine,
|
|
118
|
+
} = require("./app-port-drift");
|
|
108
119
|
// Shared fleet-roster writer/reader. `cloud:up` rosters the VM it just
|
|
109
120
|
// provisioned so the dashboard, the cloudflared-monitor tunnel publisher, and
|
|
110
121
|
// the watchdog see it within one poll cycle — without a manual fleet-vms.json
|
|
@@ -3031,32 +3042,66 @@ function buildVmAppPortReadCommand(repoName) {
|
|
|
3031
3042
|
* `VITE_PORT` is genuinely correct there (it is what `resolved_port()` lands
|
|
3032
3043
|
* on) and stays quiet. */
|
|
3033
3044
|
async function readVmAppPort(cfg) {
|
|
3034
|
-
const
|
|
3035
|
-
|
|
3036
|
-
buildGcloudArgs("ssh-probe", cfg, {
|
|
3037
|
-
command: buildVmAppPortReadCommand(repoName),
|
|
3038
|
-
}),
|
|
3039
|
-
{ stdio: ["ignore", "pipe", "ignore"] },
|
|
3040
|
-
);
|
|
3041
|
-
if (read.status !== 0 || typeof read.stdout !== "string") {
|
|
3045
|
+
const { port, failure } = await probeVmAppPort(cfg);
|
|
3046
|
+
if (failure && failure.kind === "ssh") {
|
|
3042
3047
|
console.error(
|
|
3043
|
-
`[cloud] WARNING:
|
|
3048
|
+
`[cloud] WARNING: ${failure.what} — ` +
|
|
3044
3049
|
`falling back to ${VITE_PORT}. If the app does not serve on ` +
|
|
3045
3050
|
`${VITE_PORT}, the laptop port-forward targets the wrong port and the ` +
|
|
3046
3051
|
`Live Preview will show "localhost refused to connect".`,
|
|
3047
3052
|
);
|
|
3048
|
-
|
|
3049
|
-
}
|
|
3050
|
-
if (read.stdout.includes(VM_APP_PORT_NO_CONFIG)) {
|
|
3053
|
+
} else if (failure) {
|
|
3051
3054
|
console.error(
|
|
3052
|
-
`[cloud] WARNING:
|
|
3053
|
-
`VM (looked for '${repoName}', then any project dir) — falling back to ` +
|
|
3055
|
+
`[cloud] WARNING: ${failure.what} — falling back to ` +
|
|
3054
3056
|
`${VITE_PORT}. If the project is scaffolded under a different name and ` +
|
|
3055
3057
|
`serves on another port, the port-forward targets the wrong port and ` +
|
|
3056
3058
|
`the Live Preview will show "localhost refused to connect".`,
|
|
3057
3059
|
);
|
|
3058
|
-
return VITE_PORT;
|
|
3059
3060
|
}
|
|
3061
|
+
return port == null ? VITE_PORT : port;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/** The probe behind `readVmAppPort`, without the fallback: resolves to
|
|
3065
|
+
* `{ port, failure: null }` when the VM's config was read, or
|
|
3066
|
+
* `{ port: null, failure: { kind: "ssh" | "no-config", what } }` when it was
|
|
3067
|
+
* not. Callers that must not act on a guess — the ingress reconcile, where a
|
|
3068
|
+
* 5173 fallback would wire a preview NEG endpoint to a port nothing publishes —
|
|
3069
|
+
* use this to tell a resolved port from a fallback. Prints nothing; `what` is
|
|
3070
|
+
* the human description of the miss for the caller's own warning. */
|
|
3071
|
+
async function probeVmAppPort(cfg) {
|
|
3072
|
+
const repoName = resolveProject(cfg).name;
|
|
3073
|
+
const read = runGcloud(
|
|
3074
|
+
buildGcloudArgs("ssh-probe", cfg, {
|
|
3075
|
+
command: buildVmAppPortReadCommand(repoName),
|
|
3076
|
+
}),
|
|
3077
|
+
{ stdio: ["ignore", "pipe", "ignore"] },
|
|
3078
|
+
);
|
|
3079
|
+
if (read.status !== 0 || typeof read.stdout !== "string") {
|
|
3080
|
+
return {
|
|
3081
|
+
port: null,
|
|
3082
|
+
failure: { kind: "ssh", what: "could not read the VM's app port over SSH" },
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
const { port, miss } = parseVmDeclaredAppPort(read.stdout);
|
|
3086
|
+
if (miss === "no-config") {
|
|
3087
|
+
return {
|
|
3088
|
+
port: null,
|
|
3089
|
+
failure: {
|
|
3090
|
+
kind: "no-config",
|
|
3091
|
+
what:
|
|
3092
|
+
`no .codeyam/editor.json found under ~/projects on the ` +
|
|
3093
|
+
`VM (looked for '${repoName}', then any project dir)`,
|
|
3094
|
+
},
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
return { port, failure: null };
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
/** Resolve `buildVmAppPortReadCommand`'s output into `{ port, miss }` — shared
|
|
3101
|
+
* by `probeVmAppPort` and the port-drift probe, so both resolve the declared
|
|
3102
|
+
* port identically. A no-config VM is a miss with a null port, never 5173. */
|
|
3103
|
+
function parseVmDeclaredAppPort(stdout) {
|
|
3104
|
+
if (stdout.includes(VM_APP_PORT_NO_CONFIG)) return { port: null, miss: "no-config" };
|
|
3060
3105
|
const parse = (s) => {
|
|
3061
3106
|
try {
|
|
3062
3107
|
const v = JSON.parse(s);
|
|
@@ -3065,8 +3110,116 @@ async function readVmAppPort(cfg) {
|
|
|
3065
3110
|
return {};
|
|
3066
3111
|
}
|
|
3067
3112
|
};
|
|
3068
|
-
const [baseJson = "", localJson = ""] =
|
|
3069
|
-
return resolveConfiguredAppPort(parse(baseJson), parse(localJson));
|
|
3113
|
+
const [baseJson = "", localJson = ""] = stdout.split("<<<LOCAL>>>");
|
|
3114
|
+
return { port: resolveConfiguredAppPort(parse(baseJson), parse(localJson)), miss: null };
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
/** Separates the declared-port read from the `docker ps` read in the combined
|
|
3118
|
+
* probe below. */
|
|
3119
|
+
const VM_PUBLISHED_PORT_MARKER = "<<<PUBLISHED>>>";
|
|
3120
|
+
|
|
3121
|
+
/** Read the declared app port AND the host port the VM's editor container
|
|
3122
|
+
* publishes (live `docker ps` bindings) in ONE SSH round-trip, so the roster
|
|
3123
|
+
* pass costs one extra probe per VM. Either side is null when unreadable. */
|
|
3124
|
+
function readVmAppPortsForDrift(cfg) {
|
|
3125
|
+
const read = runGcloud(
|
|
3126
|
+
buildGcloudArgs("ssh-probe", cfg, {
|
|
3127
|
+
command:
|
|
3128
|
+
`${buildVmAppPortReadCommand(resolveProject(cfg).name)}; ` +
|
|
3129
|
+
`echo '${VM_PUBLISHED_PORT_MARKER}'; ${buildPublishedAppPortReadCommand()}`,
|
|
3130
|
+
}),
|
|
3131
|
+
{ stdio: ["ignore", "pipe", "ignore"] },
|
|
3132
|
+
);
|
|
3133
|
+
if (read.status !== 0) return { declared: null, published: null };
|
|
3134
|
+
return parseVmAppPortsForDrift(read.stdout);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
/** Split the combined probe's output into `{ declared, published }`. Output
|
|
3138
|
+
* without the marker — an SSH session that exited 0 having printed nothing, a
|
|
3139
|
+
* truncated read — is unreadable on BOTH sides. Resolving its empty config
|
|
3140
|
+
* half would land on the 5173 default and could report a drift that does not
|
|
3141
|
+
* exist. */
|
|
3142
|
+
function parseVmAppPortsForDrift(stdout) {
|
|
3143
|
+
if (typeof stdout !== "string" || !stdout.includes(VM_PUBLISHED_PORT_MARKER)) {
|
|
3144
|
+
return { declared: null, published: null };
|
|
3145
|
+
}
|
|
3146
|
+
const [declaredOut, publishedOut] = stdout.split(VM_PUBLISHED_PORT_MARKER);
|
|
3147
|
+
return {
|
|
3148
|
+
declared: declaredOut.trim() ? parseVmDeclaredAppPort(declaredOut).port : null,
|
|
3149
|
+
published: parsePublishedAppPort(publishedOut),
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
/** Read the VM's `cloud-session-idle.sh` verdict (`idle` | `active` | null).
|
|
3154
|
+
* Unknown — including a VM whose checkout predates the script — is null, and
|
|
3155
|
+
* null is never treated as idle. */
|
|
3156
|
+
function readVmIdleVerdict(cfg) {
|
|
3157
|
+
const read = runGcloud(
|
|
3158
|
+
buildGcloudArgs("ssh-probe", cfg, {
|
|
3159
|
+
command:
|
|
3160
|
+
`s="$HOME/codeyam-editor/scripts/cloud-session-idle.sh"; ` +
|
|
3161
|
+
`if [ -r "$s" ]; then bash "$s" 2>/dev/null; fi; true`,
|
|
3162
|
+
}),
|
|
3163
|
+
{ stdio: ["ignore", "pipe", "ignore"] },
|
|
3164
|
+
);
|
|
3165
|
+
return read.status === 0 ? parseIdleVerdict(read.stdout) : null;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
/** Compare one VM's declared app port against its published host port over the
|
|
3169
|
+
* SSH probe. Returns the findings plus both raw reads (for the status line).
|
|
3170
|
+
* See npm/app-port-drift.js for the rule. */
|
|
3171
|
+
async function collectVmAppPortDrift(cfg, vmNumber, slug) {
|
|
3172
|
+
const { declared, published } = readVmAppPortsForDrift(cfg);
|
|
3173
|
+
const findings = await collectAppPortDrift(
|
|
3174
|
+
{ vmNumber, slug },
|
|
3175
|
+
{
|
|
3176
|
+
readDeclaredPort: async () => declared,
|
|
3177
|
+
readPublishedPort: async () => published,
|
|
3178
|
+
readIdleVerdict: async () => readVmIdleVerdict(cfg),
|
|
3179
|
+
},
|
|
3180
|
+
);
|
|
3181
|
+
return { findings, declared, published };
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/** Port-drift pass over the whole roster, on the periodic reconcile that already
|
|
3185
|
+
* walks it. Reports, never repairs (the fix recreates the container or changes
|
|
3186
|
+
* the project's port — both the operator's call). Per-VM failures are logged
|
|
3187
|
+
* and swallowed, like the preview-origin heal beside it. */
|
|
3188
|
+
async function reportAppPortDriftAcrossRoster(cfg, roster, deps = {}) {
|
|
3189
|
+
const collect = deps.collectVmAppPortDrift || collectVmAppPortDrift;
|
|
3190
|
+
const drifted = [];
|
|
3191
|
+
for (const n of roster) {
|
|
3192
|
+
const perVmCfg = { ...cfg, instanceName: `${fleetInstancePrefix()}${n}` };
|
|
3193
|
+
try {
|
|
3194
|
+
const { findings } = await collect(perVmCfg, n, slugFor(n));
|
|
3195
|
+
for (const f of findings) {
|
|
3196
|
+
console.error(formatAppPortDriftWarning(f));
|
|
3197
|
+
drifted.push(f);
|
|
3198
|
+
}
|
|
3199
|
+
} catch (e) {
|
|
3200
|
+
console.error(` VM-${n} ${slugFor(n) || `vm-${n}`}: port-drift check skipped (${e && e.message})`);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
return drifted;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
/** `npm run cloud:app-port-drift -- --instance-name <name>` — one VM's
|
|
3207
|
+
* declared-vs-published comparison as the single `port-drift=` line
|
|
3208
|
+
* `vm-ingress.sh status` appends, plus the human warning on stderr. */
|
|
3209
|
+
async function cmdAppPortDrift(args, projectDir = rootDir) {
|
|
3210
|
+
const flags = parseFlags(args);
|
|
3211
|
+
const cfg = { ...(loadCloudConfig(projectDir) || {}), ...flags };
|
|
3212
|
+
const vmNumber = resolveFleetVmNumber(cfg.instanceName || "");
|
|
3213
|
+
if (vmNumber == null) {
|
|
3214
|
+
throw new Error("cloud:app-port-drift requires --instance-name <numbered fleet VM>.");
|
|
3215
|
+
}
|
|
3216
|
+
const { findings, declared, published } = await collectVmAppPortDrift(
|
|
3217
|
+
cfg,
|
|
3218
|
+
vmNumber,
|
|
3219
|
+
slugFor(vmNumber),
|
|
3220
|
+
);
|
|
3221
|
+
for (const f of findings) console.error(formatAppPortDriftWarning(f));
|
|
3222
|
+
console.log(formatPortDriftStatusLine(findings, { declared, published }));
|
|
3070
3223
|
}
|
|
3071
3224
|
|
|
3072
3225
|
/** Build the shell command that seeds a JSON patch into a host-side
|
|
@@ -3987,7 +4140,7 @@ function fleetInstancePrefix() {
|
|
|
3987
4140
|
* (the VM is provisioned; ingress can be re-wired), Destroy likewise warns so a
|
|
3988
4141
|
* teardown still removes the instance. `vmAccess` (up only) is the Add's intended
|
|
3989
4142
|
* principal set, which the script's grant step uses in place of its sidecar read. */
|
|
3990
|
-
function runVmIngress(cfg, action, vmNumber, domain, appPort, slug, vmAccess) {
|
|
4143
|
+
function runVmIngress(cfg, action, vmNumber, domain, appPort, slug, vmAccess, { appPortUnresolved = false } = {}) {
|
|
3991
4144
|
const scriptPath = path.join(rootDir, "scripts", "gcp", "vm-ingress.sh");
|
|
3992
4145
|
const inv = vmIngressInvocation({
|
|
3993
4146
|
scriptPath,
|
|
@@ -4000,6 +4153,7 @@ function runVmIngress(cfg, action, vmNumber, domain, appPort, slug, vmAccess) {
|
|
|
4000
4153
|
instancePrefix: fleetInstancePrefix(),
|
|
4001
4154
|
slug,
|
|
4002
4155
|
vmAccess,
|
|
4156
|
+
appPortUnresolved,
|
|
4003
4157
|
});
|
|
4004
4158
|
return spawnSync(inv.command, inv.args, {
|
|
4005
4159
|
stdio: "inherit",
|
|
@@ -5117,6 +5271,25 @@ async function healPreviewOriginsAcrossRoster(cfg, roster) {
|
|
|
5117
5271
|
* resources already present and creates only the missing backend / host rule.
|
|
5118
5272
|
* A no-op in legacy quick-tunnel mode (no fleet domain). Invoked on demand and
|
|
5119
5273
|
* automatically at dashboard boot so a fleet that lost ingress self-heals. */
|
|
5274
|
+
/** Resolve one reconcile target's app port for `vm-ingress.sh up`, refusing to
|
|
5275
|
+
* guess. Resolves `{ appPort, appPortUnresolved: false }` on a real read, or
|
|
5276
|
+
* `{ appPort: null, appPortUnresolved: true }` — with a warning naming the VM —
|
|
5277
|
+
* when the probe failed, so the script leaves the preview NEG's endpoints alone
|
|
5278
|
+
* instead of wiring (and pruning to) a 5173 fallback over a working endpoint.
|
|
5279
|
+
* `deps.probeVmAppPort` is injectable so tests never reach for SSH. */
|
|
5280
|
+
async function resolveReconcileAppPort(perVmCfg, vmNumber, deps = {}) {
|
|
5281
|
+
const probe = deps.probeVmAppPort || probeVmAppPort;
|
|
5282
|
+
const { port, failure } = await probe(perVmCfg);
|
|
5283
|
+
if (!failure) return { appPort: port, appPortUnresolved: false };
|
|
5284
|
+
console.error(
|
|
5285
|
+
`[cloud] WARNING: VM-${vmNumber}: ${failure.what} — skipping the preview ` +
|
|
5286
|
+
`endpoint converge rather than wiring ${VITE_PORT} over a working endpoint. ` +
|
|
5287
|
+
`If the preview NEG targets the wrong port, Live Preview requests fail ` +
|
|
5288
|
+
`with "upstream connect error" until a later reconcile resolves it.`,
|
|
5289
|
+
);
|
|
5290
|
+
return { appPort: null, appPortUnresolved: true };
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5120
5293
|
async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
5121
5294
|
ensureGcloud();
|
|
5122
5295
|
parseFlags(args);
|
|
@@ -5189,17 +5362,23 @@ async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
|
5189
5362
|
for (const t of targets) {
|
|
5190
5363
|
const label = t.slug || `vm-${t.vmNumber}`;
|
|
5191
5364
|
// Resolve the VM's real dev-server port so a re-created preview NEG targets
|
|
5192
|
-
// the right port
|
|
5193
|
-
//
|
|
5365
|
+
// the right port. NOT readVmAppPort: its VITE_PORT fallback on an SSH/parse
|
|
5366
|
+
// failure is indistinguishable from a VM genuinely serving 5173, and `up`
|
|
5367
|
+
// prunes the preview NEG to the port it is given — a fallback would detach the
|
|
5368
|
+
// working endpoint and wire one nothing publishes. Unresolved → the script
|
|
5369
|
+
// leaves the preview endpoints alone; every other resource still converges.
|
|
5194
5370
|
const perVmCfg = { ...cfg, instanceName: `${fleetInstancePrefix()}${t.vmNumber}` };
|
|
5195
|
-
const appPort = await
|
|
5196
|
-
const res = runVmIngress(perVmCfg, "up", t.vmNumber, domain, appPort, t.slug
|
|
5371
|
+
const { appPort, appPortUnresolved } = await resolveReconcileAppPort(perVmCfg, t.vmNumber);
|
|
5372
|
+
const res = runVmIngress(perVmCfg, "up", t.vmNumber, domain, appPort, t.slug, undefined, {
|
|
5373
|
+
appPortUnresolved,
|
|
5374
|
+
});
|
|
5197
5375
|
console.error(
|
|
5198
5376
|
` VM-${t.vmNumber} ${label}: ${t.verdict} → ${res.status === 0 ? "re-wired" : "re-wire FAILED"}`,
|
|
5199
5377
|
);
|
|
5200
5378
|
}
|
|
5201
5379
|
|
|
5202
5380
|
await healPreviewOriginsAcrossRoster(cfg, roster);
|
|
5381
|
+
await reportAppPortDriftAcrossRoster(cfg, roster);
|
|
5203
5382
|
|
|
5204
5383
|
// Converge each rostered backend-service's timeout to 86400 (24h). A fully-wired
|
|
5205
5384
|
// BES still at GCP's 30s default reports `wired` above and is skipped by the
|
|
@@ -5768,6 +5947,7 @@ const HANDLERS = {
|
|
|
5768
5947
|
status: cmdStatus,
|
|
5769
5948
|
"ingress-up": cmdIngressUp,
|
|
5770
5949
|
"reconcile-ingress": cmdReconcileIngress,
|
|
5950
|
+
"app-port-drift": cmdAppPortDrift,
|
|
5771
5951
|
"wire-add-tail": cmdWireAddTail,
|
|
5772
5952
|
"reconcile-credentials": cmdReconcileCredentials,
|
|
5773
5953
|
"reconcile-admin": cmdReconcileAdmin,
|
|
@@ -5862,6 +6042,8 @@ module.exports = {
|
|
|
5862
6042
|
seedPreviewOrigin,
|
|
5863
6043
|
healPreviewOriginAtReady,
|
|
5864
6044
|
healPreviewOriginsAcrossRoster,
|
|
6045
|
+
reportAppPortDriftAcrossRoster,
|
|
6046
|
+
cmdAppPortDrift,
|
|
5865
6047
|
buildGcloudArgs,
|
|
5866
6048
|
sshScriptCommand,
|
|
5867
6049
|
bootstrapRunnerScript,
|
|
@@ -5951,6 +6133,10 @@ module.exports = {
|
|
|
5951
6133
|
classifyGitRemoteError,
|
|
5952
6134
|
decideCredentialHeal,
|
|
5953
6135
|
readVmAppPort,
|
|
6136
|
+
probeVmAppPort,
|
|
6137
|
+
resolveReconcileAppPort,
|
|
6138
|
+
parseVmDeclaredAppPort,
|
|
6139
|
+
parseVmAppPortsForDrift,
|
|
5954
6140
|
buildVmAppPortReadCommand,
|
|
5955
6141
|
resolveLaptopGitIdentity,
|
|
5956
6142
|
gitBranchRefExists,
|
package/npm/ingress-invoke.js
CHANGED
|
@@ -86,7 +86,11 @@ function regionFromZone(zone) {
|
|
|
86
86
|
* ingress path granting a reused slot's prior tenant that the very next step then
|
|
87
87
|
* revokes as stale (VMs 4/5/6, 2026-08-09). Omit it (or pass an empty list) to
|
|
88
88
|
* leave the script's sidecar-read fallback in place — the standalone/reconcile
|
|
89
|
-
* path, where no Add is deciding the intent.
|
|
89
|
+
* path, where no Add is deciding the intent.
|
|
90
|
+
*
|
|
91
|
+
* `appPortUnresolved` (`up` only) becomes APP_PORT_UNRESOLVED=1: the caller tried
|
|
92
|
+
* to resolve the app port and could not, so the script leaves the preview NEG's
|
|
93
|
+
* endpoints untouched instead of wiring (and pruning to) a guessed port. */
|
|
90
94
|
function vmIngressInvocation({
|
|
91
95
|
scriptPath,
|
|
92
96
|
vmNumber,
|
|
@@ -98,6 +102,7 @@ function vmIngressInvocation({
|
|
|
98
102
|
instancePrefix,
|
|
99
103
|
slug,
|
|
100
104
|
vmAccess,
|
|
105
|
+
appPortUnresolved = false,
|
|
101
106
|
} = {}) {
|
|
102
107
|
if (!scriptPath) throw new Error('vmIngressInvocation requires scriptPath');
|
|
103
108
|
if (action !== 'up' && action !== 'down') {
|
|
@@ -116,6 +121,7 @@ function vmIngressInvocation({
|
|
|
116
121
|
if (slug != null && String(slug).trim() !== '') env.VM_SLUG = String(slug).trim();
|
|
117
122
|
const access = (Array.isArray(vmAccess) ? vmAccess.join(',') : String(vmAccess == null ? '' : vmAccess)).trim();
|
|
118
123
|
if (action === 'up' && access !== '') env.VM_ACCESS = access;
|
|
124
|
+
if (action === 'up' && appPortUnresolved) env.APP_PORT_UNRESOLVED = '1';
|
|
119
125
|
return { command: 'bash', args: [scriptPath, String(n), action], env };
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -141,7 +147,10 @@ function vmIngressStatusInvocation({
|
|
|
141
147
|
throw new Error(`vm-ingress status requires a positive VM number, got: ${vmNumber}`);
|
|
142
148
|
}
|
|
143
149
|
if (!domain) throw new Error('vmIngressStatusInvocation requires a fleet domain');
|
|
144
|
-
|
|
150
|
+
// Machine callers poll `status` for the LB verdict only; the port-drift field
|
|
151
|
+
// costs an SSH round-trip per call, and the roster reconcile runs that
|
|
152
|
+
// comparison itself. An operator running the script by hand still gets it.
|
|
153
|
+
const env = { FLEET_DOMAIN: domain, VM_INGRESS_SKIP_PORT_DRIFT: '1' };
|
|
145
154
|
if (project) env.GCP_PROJECT = project;
|
|
146
155
|
if (zone) env.ZONE = zone;
|
|
147
156
|
if (slug != null && String(slug).trim() !== '') env.VM_SLUG = String(slug).trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeyam-editor/codeyam-editor",
|
|
3
|
-
"version": "0.1.13
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Language-agnostic managed execution sandbox for scenario-driven development",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codeyam-editor": "./npm/cli.js"
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"playwright": "^1.58.2"
|
|
10
10
|
},
|
|
11
11
|
"optionalDependencies": {
|
|
12
|
-
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.13
|
|
13
|
-
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.13
|
|
14
|
-
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.13
|
|
15
|
-
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.13
|
|
12
|
+
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.13",
|
|
13
|
+
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.13",
|
|
14
|
+
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.13",
|
|
15
|
+
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.13"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"codeyam",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-DNb1olaj.js";import{k as E}from"./index-
|
|
1
|
+
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-DNb1olaj.js";import{k as E}from"./index-mgNPDFlH.js";function P({connected:t}){return e.jsx("span",{className:`inline-block rounded-full px-2 py-0.5 text-xs font-medium text-[var(--text-primary)] ${t?"bg-[var(--accent-green)]":"bg-[var(--accent-red)]"}`,children:t?"connected":"reconnecting…"})}const re=Object.freeze(Object.defineProperty({__proto__:null,ConnectionBadge:P},Symbol.toStringTag,{value:"Module"}));function N(){const[t,r]=a.useState(null),[o,s]=a.useState(!0),{events:l}=k(),n=a.useCallback(async()=>{try{const c=await(await fetch("/api/inspector/status")).json();r(c)}catch{}finally{s(!1)}},[]);return a.useEffect(()=>{n()},[n]),a.useEffect(()=>{const i=l[l.length-1];i&&i.type==="scenario_switch"&&n()},[l,n]),{scenario:(t==null?void 0:t.scenario)??null,previewScenario:(t==null?void 0:t.previewScenario)??null,routes:(t==null?void 0:t.routes)??[],routeCount:(t==null?void 0:t.routeCount)??0,loading:o,refetch:n}}function v({title:t}){return e.jsx("h2",{style:{fontSize:14,fontWeight:600,color:"#f0f6fc",margin:"0 0 8px 0",borderBottom:"1px solid #30363d",paddingBottom:8},children:t})}function f({color:t,children:r}){return e.jsx("span",{style:{background:`${t}22`,color:t,border:`1px solid ${t}55`,borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,letterSpacing:"0.5px",whiteSpace:"nowrap"},children:r})}const $={GET:"#D7FF63",POST:"#238636",PUT:"#d29922",DELETE:"#da3633",PATCH:"#bc8cff"};function O({method:t}){const r=$[t.toUpperCase()]??"#8b949e";return e.jsx("span",{style:{color:r,fontWeight:600,fontSize:11,minWidth:48,display:"inline-block"},children:t.toUpperCase()})}function C({color:t,children:r}){return e.jsx("div",{style:{padding:"12px 16px",borderRadius:6,border:`1px solid ${t}44`,background:`${t}11`,color:t},children:r})}const u={padding:"8px 12px",textAlign:"center",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},h={padding:"6px 12px"},S={background:"#161b22",border:"1px solid #30363d",borderRadius:6,color:"#c9d1d9",padding:"6px 12px",fontSize:13,fontFamily:"inherit"},R={...S,appearance:"none",paddingRight:24,backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b949e' d='M6 8L1 3h10z'/%3E%3C/svg%3E")`,backgroundRepeat:"no-repeat",backgroundPosition:"right 8px center"},oe=Object.freeze(Object.defineProperty({__proto__:null,Badge:f,MethodBadge:O,SectionHeader:v,StatusBox:C,inputStyle:S,selectStyle:R,tdStyle:h,thStyle:u},Symbol.toStringTag,{value:"Module"}));function z({routes:t}){return e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d"},children:[e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"Pattern"}),e.jsx("th",{style:u,children:"Status"})]})}),e.jsx("tbody",{children:t.map((r,o)=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:r.method})}),e.jsx("td",{style:h,children:r.pattern}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:r.status})]},o))})]})}const se=Object.freeze(Object.defineProperty({__proto__:null,InspectorRouteTable:z},Symbol.toStringTag,{value:"Module"}));function A(){const{scenario:t,previewScenario:r,routes:o,routeCount:s,loading:l}=N();return e.jsxs("section",{children:[e.jsx(v,{title:"Active Scenario"}),l?e.jsx(C,{color:"#8b949e",children:"Loading..."}):t?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#238636",children:"ACTIVE"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:t.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",t.slug,")"]})]}),e.jsxs("div",{style:{color:"#8b949e",fontSize:12},children:[s," mock route",s!==1?"s":""," loaded"]}),o.length>0&&e.jsx(z,{routes:o})]}):r?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#D7FF63",children:"PREVIEW"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:r.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",r.slug,")"]})]}),e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"Component preview — no HTTP mocks active"})]}):e.jsx(C,{color:"#d29922",children:"No scenario active — all requests pass through to upstream"})]})}const ne=Object.freeze(Object.defineProperty({__proto__:null,InspectorScenarioSection:A},Symbol.toStringTag,{value:"Module"}));function F({textUrlFilter:t,onTextUrlFilterChange:r,methodFilter:o,onMethodFilterChange:s,sourceFilter:l,onSourceFilterChange:n}){return e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12,alignItems:"center"},children:[e.jsx("input",{type:"text",placeholder:"Filter by URL...",value:t,onChange:i=>r(i.target.value),style:{...S,flex:1,minWidth:0}}),e.jsxs("select",{value:o,onChange:i=>s(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Methods"}),e.jsx("option",{value:"GET",children:"GET"}),e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"}),e.jsx("option",{value:"DELETE",children:"DELETE"}),e.jsx("option",{value:"PATCH",children:"PATCH"})]}),e.jsxs("select",{value:l,onChange:i=>n(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Sources"}),e.jsx("option",{value:"MOCKED",children:"Mocked"}),e.jsx("option",{value:"PASSTHROUGH",children:"Passthrough"})]})]})}const ie=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficFilterBar:F},Symbol.toStringTag,{value:"Module"})),G={color:"#8b949e",fontSize:11,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:4},M={background:"#161b22",border:"1px solid #30363d",borderRadius:4,padding:"8px 12px",margin:0,fontSize:11,color:"#c9d1d9",fontFamily:"monospace",maxHeight:200,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"};function _({label:t,children:r}){return e.jsxs("div",{style:{marginBottom:12},children:[e.jsx("div",{style:G,children:t}),r]})}function B({event:t}){let r=[];try{const n=new URL(t.url,"http://localhost");r=Array.from(n.searchParams.entries())}catch{}const o=t.requestHeaders&&Object.keys(t.requestHeaders).length>0,s=t.requestBody!=null,l=t.responseBody!=null;return e.jsxs("div",{style:{background:"#0d1117",padding:"12px 16px",borderTop:"1px solid #30363d"},children:[r.length>0&&e.jsx(_,{label:"Query Parameters",children:e.jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 12px",fontSize:12},children:r.map(([n,i],c)=>e.jsxs("div",{style:{display:"contents"},children:[e.jsx("span",{style:{color:"#D7FF63",fontFamily:"monospace"},children:n}),e.jsx("span",{style:{color:"#c9d1d9",fontFamily:"monospace"},children:i})]},c))})}),o&&e.jsx(_,{label:"Request Headers",children:e.jsx("pre",{style:M,children:Object.entries(t.requestHeaders).map(([n,i])=>`${n}: ${i}`).join(`
|
|
2
2
|
`)})}),s&&e.jsx(_,{label:"Request Body",children:e.jsx("pre",{style:M,children:typeof t.requestBody=="string"?t.requestBody:JSON.stringify(t.requestBody,null,2)})}),l&&e.jsx(_,{label:"Response Body",children:e.jsx("pre",{style:M,children:typeof t.responseBody=="string"?t.responseBody:JSON.stringify(t.responseBody,null,2)})}),r.length===0&&!o&&!s&&!l&&e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"No additional details available for this request"})]})}const le=Object.freeze(Object.defineProperty({__proto__:null,DetailSection:_,InspectorTrafficDetail:B},Symbol.toStringTag,{value:"Module"}));function L({event:t}){const[r,o]=a.useState(!1),s=new Date(t.timestampMs).toLocaleTimeString(),l=t.mocked;return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{onClick:()=>o(!r),style:{borderBottom:"1px solid #21262d",background:l?"transparent":"rgba(210, 153, 34, 0.05)",cursor:"pointer"},children:[e.jsxs("td",{style:{...h,color:"#8b949e",textAlign:"center"},children:[e.jsx("span",{style:{display:"inline-block",width:12,marginRight:4,fontSize:10,color:"#8b949e",transition:"transform 0.15s",transform:r?"rotate(90deg)":"rotate(0deg)"},children:"▶"}),s]}),e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:t.method})}),e.jsx("td",{style:{...h,maxWidth:400,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.url}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:t.status??"---"}),e.jsxs("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:[t.durationMs,"ms"]}),e.jsx("td",{style:{...h,textAlign:"center"},children:l?t.mockSource==="default"?e.jsx(f,{color:"#1f6feb",children:"MOCKED (default)"}):e.jsx(f,{color:"#238636",children:"MOCKED (scenario)"}):e.jsx(f,{color:"#d29922",children:"PASSTHROUGH"})})]}),r&&e.jsx("tr",{children:e.jsx("td",{colSpan:6,style:{padding:0},children:e.jsx(B,{event:t})})})]})}const ae=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficRow:L},Symbol.toStringTag,{value:"Module"}));function D({events:t}){return e.jsx("div",{style:{maxHeight:300,overflowY:"auto",border:"1px solid #30363d",borderRadius:6},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",position:"sticky",top:0,background:"#0d1117"},children:[e.jsx("th",{style:u,children:"Time"}),e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"URL"}),e.jsx("th",{style:u,children:"Status"}),e.jsx("th",{style:u,children:"Duration"}),e.jsx("th",{style:u,children:"Source"})]})}),e.jsx("tbody",{children:t.map(r=>e.jsx(L,{event:r},r.id))})]})})}const ce=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficTable:D},Symbol.toStringTag,{value:"Module"}));function K(t){if(t===void 0)return"checked";if(t==="checked")return"excluded"}function I({chips:t,chipStates:r,onChipStatesChange:o}){if(t.length===0)return null;const s=n=>{const i=new Map(r),c=K(i.get(n));c===void 0?i.delete(n):i.set(n,c),o(i)},l=()=>{o(new Map)};return e.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:6,marginBottom:12},children:[r.size>0&&e.jsx("button",{onClick:l,style:{background:"#da363322",color:"#da3633",border:"1px solid #da363355",borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,cursor:"pointer",fontFamily:"inherit"},children:"Clear all"}),t.map(({path:n,count:i})=>{const c=r.get(n),d=c==="checked",g=c==="excluded";let m="#30363d",b="#8b949e",T="#30363d",y="";return d?(m="#D7FF6322",b="#D7FF63",T="#D7FF6355",y="✓ "):g&&(m="#da363322",b="#da3633",T="#da363355",y="✗ "),e.jsxs("button",{onClick:()=>s(n),style:{background:m,color:b,border:`1px solid ${T}`,borderRadius:4,padding:"2px 8px",fontSize:11,fontFamily:"monospace",cursor:"pointer",whiteSpace:"nowrap"},children:[y,n,e.jsxs("span",{style:{marginLeft:4,opacity:.6,fontFamily:"inherit"},children:["(",i,")"]})]},n)})]})}const de=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficUrlChips:I},Symbol.toStringTag,{value:"Module"}));function H(){const{events:t}=k(),[r,o]=a.useState(""),[s,l]=a.useState(()=>new Map),[n,i]=a.useState("ALL"),[c,d]=a.useState("ALL"),g=t.filter(p=>p.type==="http_request"),m=a.useMemo(()=>{const p=new Map;for(const x of g){const j=E(x.url);p.set(j,(p.get(j)||0)+1)}return Array.from(p.entries()).sort((x,j)=>j[1]-x[1]).slice(0,10).map(([x,j])=>({path:x,count:j}))},[g]),b=a.useMemo(()=>Array.from(s.values()).some(p=>p==="checked"),[s]),y=g.filter(p=>{if(r&&!p.url.toLowerCase().includes(r.toLowerCase()))return!1;const x=E(p.url);return!(s.get(x)==="excluded"||b&&s.get(x)!=="checked"||n!=="ALL"&&p.method.toUpperCase()!==n||c==="MOCKED"&&!p.mocked||c==="PASSTHROUGH"&&p.mocked)}).slice(-50).reverse();return e.jsxs("section",{children:[e.jsx(v,{title:"Live Traffic"}),e.jsx(F,{textUrlFilter:r,onTextUrlFilterChange:o,methodFilter:n,onMethodFilterChange:i,sourceFilter:c,onSourceFilterChange:p=>d(p)}),e.jsx(I,{chips:m,chipStates:s,onChipStatesChange:l}),y.length===0?e.jsx(C,{color:"#8b949e",children:g.length>0?"No matching traffic — try adjusting the filters":"No HTTP traffic yet — requests will appear here in real-time"}):e.jsx(D,{events:y})]})}const pe=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficSection:H},Symbol.toStringTag,{value:"Module"}));function J(){const[t,r]=a.useState(null),[o,s]=a.useState(!1);return{explain:a.useCallback(async(n,i)=>{s(!0);try{const d=await(await fetch("/api/inspector/explain",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:n,url:i})})).json();r(d)}catch{}finally{s(!1)}},[]),result:t,loading:o}}function U({result:t}){return e.jsxs("div",{style:{border:"1px solid #30363d",borderRadius:6,padding:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:12},children:[e.jsxs("span",{style:{color:"#f0f6fc",fontWeight:500},children:[t.method," ",t.url]}),t.wouldMatch?e.jsx(f,{color:"#238636",children:"WOULD MATCH"}):e.jsx(f,{color:"#da3633",children:"NO MATCH"})]}),t.attempts.length===0?e.jsx("div",{style:{color:"#8b949e"},children:"No routes loaded to match against"}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6},children:t.attempts.map((r,o)=>e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 8px",borderRadius:4,background:r.failure?"transparent":"rgba(35, 134, 54, 0.1)"},children:[e.jsx("span",{style:{color:r.failure?"#da3633":"#238636",fontWeight:600,width:16},children:r.failure?"x":"v"}),e.jsx(O,{method:r.method}),e.jsx("span",{children:r.pattern}),r.failure&&e.jsx("span",{style:{color:"#8b949e",fontSize:11},children:r.failure.reason==="methodMismatch"?`method: expected ${r.failure.expected}, got ${r.failure.actual}`:`path "${r.failure.path}" does not match "${r.failure.pattern}"`})]},o))})]})}const ue=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerResult:U},Symbol.toStringTag,{value:"Module"}));function W(){const{explain:t,result:r,loading:o}=J(),[s,l]=a.useState("GET"),[n,i]=a.useState(""),c=d=>{d.preventDefault(),n.trim()&&t(s,n.trim())};return e.jsxs("section",{children:[e.jsx(v,{title:"Match Explainer"}),e.jsxs("form",{onSubmit:c,style:{display:"flex",gap:8,marginBottom:12},children:[e.jsx("select",{value:s,onChange:d=>l(d.target.value),style:{...S,width:100},children:["GET","POST","PUT","DELETE","PATCH"].map(d=>e.jsx("option",{value:d,children:d},d))}),e.jsx("input",{type:"text",value:n,onChange:d=>i(d.target.value),placeholder:"/api/users",style:{...S,flex:1}}),e.jsx("button",{type:"submit",disabled:o||!n.trim(),style:{background:"#238636",color:"#fff",border:"none",borderRadius:6,padding:"6px 16px",cursor:"pointer",fontSize:12,fontWeight:500,opacity:o||!n.trim()?.5:1},children:o?"Checking...":"Explain"})]}),r&&e.jsx(U,{result:r})]})}const he=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerSection:W},Symbol.toStringTag,{value:"Module"})),w={"/api/health":"Health check — confirms the proxy and mock engine are running","/api/events":"SSE stream of real-time events (traffic, errors) powering this Inspector","/api/config":"Returns current CodeYam configuration for the project","/api/editor-dev-server":"Polls/controls the dev server status (start, stop, restart)","/api/inspector/status":"Returns active scenario and loaded mock routes","/api/inspector/explain":"Tests which mock route would match a given method + URL","/api/scenarios":"Lists all defined scenarios with metadata","/api/tests":"Returns the AI-maintained test registry joined with per-test-evidence, grouped by file","/api/test-results":"Returns latest test execution results","/api/screenshots/*":"Serves captured scenario screenshots","/api/session-info":"Returns current editor session metadata","/api/data-structure":"Returns the app's data structure definition","/api/dependency-graph":"Returns the project's component dependency graph","/api/app-route-entities":"Lists app routes and their associated entities","/api/glossary":"Returns the project's component glossary","/api/step":"Returns current editor workflow step","/api/editor-commands":"Lists available editor commands"};function V(t){if(w[t])return w[t];for(const[r,o]of Object.entries(w))if(r.endsWith("/*")){const s=r.slice(0,-1);if(t.startsWith(s))return o}}function Y(t){const r=new Set([...t,...Object.keys(w)]),o=[];for(const s of Array.from(r).sort()){if(s.endsWith("/*"))continue;const l=V(s);o.push({path:s,description:l??"Application endpoint"})}return o}function q(){const{events:t}=k(),r=a.useMemo(()=>{const o=[];for(const s of t)s.type==="http_request"&&o.push(E(s.url));return Y(o)},[t]);return r.length===0?null:e.jsxs("section",{children:[e.jsx(v,{title:"URL Glossary"}),e.jsx("div",{style:{border:"1px solid #30363d",borderRadius:6,overflow:"hidden"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",background:"#161b22"},children:[e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px",width:"30%"},children:"Path"}),e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},children:"Description"})]})}),e.jsx("tbody",{children:r.map(({path:o,description:s})=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{padding:"4px 12px",fontFamily:"monospace",color:"#D7FF63",fontSize:11},children:o}),e.jsx("td",{style:{padding:"4px 12px",color:"#8b949e"},children:s})]},o))})]})})]})}const xe=Object.freeze(Object.defineProperty({__proto__:null,InspectorUrlGlossary:q},Symbol.toStringTag,{value:"Module"}));function Q(){const{connected:t}=k();return e.jsxs("div",{style:{background:"#0d1117",color:"#c9d1d9",minHeight:"100vh",fontFamily:"'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace",fontSize:13,padding:24,display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("header",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("h1",{style:{fontSize:18,fontWeight:600,color:"#f0f6fc",margin:0},children:"Container Inspector"}),e.jsx("span",{style:{color:"#8b949e",fontSize:12},children:"Read-only view of proxy and mock engine state"}),e.jsx(P,{connected:t})]}),e.jsx(A,{}),e.jsx(H,{}),e.jsx(W,{}),e.jsx(q,{})]})}const fe=Object.freeze(Object.defineProperty({__proto__:null,ContainerInspector:Q},Symbol.toStringTag,{value:"Module"}));export{f as B,Q as C,_ as D,L as I,O as M,v as S,xe as _,C as a,q as b,I as c,D as d,F as e,B as f,U as g,W as h,H as i,z as j,A as k,P as l,de as m,ce as n,pe as o,ae as p,ie as q,le as r,ne as s,u as t,se as u,oe as v,he as w,ue as x,fe as y,re as z};
|