@codeyam-editor/codeyam-editor 0.1.6 → 0.1.8
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/cloud.js +308 -52
- package/npm/compose-conflict.js +59 -0
- package/npm/fleet-add-failure.js +174 -18
- package/npm/fleet-cleanup.js +129 -0
- package/npm/fleet-job-badge.js +28 -12
- package/npm/fleet-memory.js +73 -4
- package/npm/fleet-probe-transport.js +155 -0
- package/npm/fleet-queue.js +141 -0
- package/npm/fleet-revision.js +104 -0
- package/npm/fleet-running-plan.js +37 -7
- package/npm/fleet-vm-access.js +162 -0
- package/npm/ingress-invoke.js +46 -2
- package/npm/launch-config.js +163 -4
- package/npm/scenario-check.js +63 -19
- package/npm/scenario-handlers.js +18 -0
- package/npm/scenario-issues.js +11 -0
- package/npm/scenario-mocks.js +245 -39
- package/npm/scenario-playwright.js +114 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-DgM0pTAx.js → ContainerInspector-DSVZvEkB.js} +1 -1
- package/ui/dist/assets/EditorShell-DnXdHrar.js +256 -0
- package/ui/dist/assets/EditorShell-FvbXyqqD.css +1 -0
- package/ui/dist/assets/ProjectLauncherScreen-E0NYYiwy.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-6qJE-0lA.js +15 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-BLqRCxQX.js → ScenarioDataPanelFullPage-Cyk6efiI.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-Uyt54vHI.js +21 -0
- package/ui/dist/assets/{index-DCHqRrwO.js → index-BPLKgwon.js} +9 -9
- package/ui/dist/assets/index-DKpgJLo3.css +1 -0
- package/ui/dist/assets/useServerIdentity-CVuHTzjg.js +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/EditorShell-B7Q06uJ_.js +0 -252
- package/ui/dist/assets/EditorShell-DP5UY20G.css +0 -1
- package/ui/dist/assets/ProjectLauncherScreen-BRrEr8Ng.js +0 -1
- package/ui/dist/assets/ScenarioDataPanel-BeQfa28g.js +0 -9
- package/ui/dist/assets/ScenarioRenderer-CIAxfHPh.js +0 -21
- package/ui/dist/assets/index-BygCiJSb.css +0 -1
- package/ui/dist/assets/useServerIdentity-B7rI0u3O.js +0 -1
package/npm/scenario-mocks.js
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
// Route matcher
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// left doing exact string matching), a parameterized or query-spec'd mock
|
|
7
|
-
// rendered in the Live Preview but escaped Playwright's `page.route` during
|
|
8
|
-
// capture and rendered an empty list. `scenario-mocks.test.js` pins the two to
|
|
9
|
-
// identical verdicts on a shared fixture table so a future divergence is a
|
|
10
|
-
// test failure, not another lost build session.
|
|
1
|
+
// Route matcher for the capture harness — one of THREE implementations of a
|
|
2
|
+
// single semantic. The authority is the Rust engine
|
|
3
|
+
// (crates/mock-engine/src/route_parser.rs), where the contract is written down;
|
|
4
|
+
// this file and the injected live-preview shim
|
|
5
|
+
// (crates/proxy-http/src/fetch_patch.js) are ports of it.
|
|
11
6
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
7
|
+
// This is NOT a verbatim copy of the shim, and describing it as one is what let
|
|
8
|
+
// the drift hide: the two JS matchers agreed with EACH OTHER while both
|
|
9
|
+
// disagreed with the server (they prefix-matched full-URL patterns and returned
|
|
10
|
+
// before applying any query spec, where the server anchored). All three are now
|
|
11
|
+
// pinned against one shared fixture table,
|
|
12
|
+
// crates/mock-engine/fixtures/route-match-fixtures.json, asserted with its
|
|
13
|
+
// expected verdicts by both `scenario-mocks.test.js` and the Rust tests — so a
|
|
14
|
+
// change to one implementation the others do not follow is a test failure, and
|
|
15
|
+
// two matchers agreeing on a wrong answer no longer passes.
|
|
16
|
+
//
|
|
17
|
+
// The semantic, in short (see route_parser.rs for the full contract): `:name`
|
|
18
|
+
// and `*` each match one non-empty path segment; a TRAILING `**` matches the
|
|
19
|
+
// remaining path including `/` and including an empty remainder; a `?k=v` /
|
|
20
|
+
// `?k=*` suffix constrains the query as a subset match. A full-URL pattern
|
|
21
|
+
// (`http://…`) with no wildcard matches by PREFIX — the compatibility rule
|
|
22
|
+
// every shipped external mock relies on — while a wildcarded one matches by the
|
|
23
|
+
// wildcard rules.
|
|
16
24
|
function matchUrl(pattern, url) {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
// Split the query spec off FIRST — for full-URL and path-only patterns
|
|
26
|
+
// alike. Returning early for full URLs (as this did) meant a spec like
|
|
27
|
+
// `?k=*` on an external mock was silently ignored.
|
|
28
|
+
const pqi = pattern.indexOf("?");
|
|
29
|
+
const patPath = pqi >= 0 ? pattern.slice(0, pqi) : pattern;
|
|
30
|
+
const patSpec = pqi >= 0 ? pattern.slice(pqi + 1) : "";
|
|
31
|
+
|
|
32
|
+
// Full-URL patterns (external APIs) match against the whole URL, with its
|
|
33
|
+
// own query stripped before the compare.
|
|
34
|
+
if (patPath.startsWith("http://") || patPath.startsWith("https://")) {
|
|
35
|
+
const [rawPath, rawQuery] = splitUrlQuery(url);
|
|
36
|
+
// Compatibility rule: a wildcard-free full-URL pattern prefix-matches,
|
|
37
|
+
// which is what every external mock relies on today. A wildcarded one
|
|
38
|
+
// matches by the wildcard rules, with no prefix fallback.
|
|
39
|
+
const urlMatched = hasWildcard(patPath)
|
|
40
|
+
? matchPath(patPath, rawPath)
|
|
41
|
+
: rawPath.indexOf(patPath) === 0;
|
|
42
|
+
if (!urlMatched) return false;
|
|
43
|
+
return matchQuerySpec(patSpec, rawQuery);
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
let reqPath = url;
|
|
@@ -26,35 +50,71 @@ function matchUrl(pattern, url) {
|
|
|
26
50
|
reqPath = u.pathname;
|
|
27
51
|
reqQuery = u.search.replace(/^\?/, "");
|
|
28
52
|
} catch {
|
|
29
|
-
|
|
30
|
-
if (qi >= 0) {
|
|
31
|
-
reqPath = url.slice(0, qi);
|
|
32
|
-
reqQuery = url.slice(qi + 1);
|
|
33
|
-
}
|
|
53
|
+
[reqPath, reqQuery] = splitUrlQuery(url);
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
if (!matchPath(patPath, reqPath)) return false;
|
|
57
|
+
return matchQuerySpec(patSpec, reqQuery);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Split a raw URL into [everything-before-the-query, query]. The `#fragment`
|
|
61
|
+
// is dropped from both halves, mirroring the server's strip_url_query.
|
|
62
|
+
function splitUrlQuery(url) {
|
|
63
|
+
const qi = url.indexOf("?");
|
|
64
|
+
const hi = url.indexOf("#");
|
|
65
|
+
const cut = qi < 0 ? hi : hi < 0 ? qi : Math.min(qi, hi);
|
|
66
|
+
const head = cut < 0 ? url : url.slice(0, cut);
|
|
67
|
+
if (qi < 0) return [head, ""];
|
|
68
|
+
const query = url.slice(qi + 1);
|
|
69
|
+
const fi = query.indexOf("#");
|
|
70
|
+
return [head, fi >= 0 ? query.slice(0, fi) : query];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// True when a segment is a valid `:name` param. A `:`-prefixed segment with
|
|
74
|
+
// an empty or punctuated name is matched literally, same as the server.
|
|
75
|
+
function isParamSegment(seg) {
|
|
76
|
+
return /^:[\p{L}\p{N}_]+$/u.test(seg);
|
|
39
77
|
}
|
|
40
78
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
// True when a segment is any wildcard: `:name`, `*`, or `**`.
|
|
80
|
+
function isWildcardSegment(seg) {
|
|
81
|
+
return seg === "*" || seg === "**" || isParamSegment(seg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// True when the pattern contains any wildcard segment.
|
|
85
|
+
function hasWildcard(pattern) {
|
|
86
|
+
return pattern.split("/").some(isWildcardSegment);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Segment-wise path match. `:name` and `*` each match any single non-empty
|
|
90
|
+
// segment; a TRAILING `**` matches the remaining path including `/` and
|
|
91
|
+
// including an empty remainder (`/api/**` covers `/api`); every other segment
|
|
92
|
+
// must be equal. No implicit prefix match, same as the server.
|
|
44
93
|
function matchPath(pattern, path) {
|
|
45
94
|
const pp = pattern.split("/");
|
|
46
95
|
const rp = path.split("/");
|
|
96
|
+
if (pp[pp.length - 1] === "**") {
|
|
97
|
+
const head = pp.length - 1;
|
|
98
|
+
if (rp.length < head) return false;
|
|
99
|
+
for (let i = 0; i < head; i++) {
|
|
100
|
+
if (!matchSegment(pp[i], rp[i])) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
47
104
|
if (pp.length !== rp.length) return false;
|
|
48
105
|
for (let i = 0; i < pp.length; i++) {
|
|
49
|
-
if (pp[i]
|
|
50
|
-
if (rp[i] === "") return false;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (pp[i] !== rp[i]) return false;
|
|
106
|
+
if (!matchSegment(pp[i], rp[i])) return false;
|
|
54
107
|
}
|
|
55
108
|
return true;
|
|
56
109
|
}
|
|
57
110
|
|
|
111
|
+
// One segment. `:name`, `*`, and a non-final `**` each match any single
|
|
112
|
+
// non-empty segment; anything else must be equal.
|
|
113
|
+
function matchSegment(pat, seg) {
|
|
114
|
+
if (isWildcardSegment(pat)) return seg !== "";
|
|
115
|
+
return pat === seg;
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
// Query-spec match. An empty spec matches any query (a path-only route still
|
|
59
119
|
// matches a request that carries query params). Otherwise every `k=v` pair in
|
|
60
120
|
// the spec must be present in the request query; `k=*` matches any value.
|
|
@@ -82,7 +142,11 @@ function splitMockKey(key) {
|
|
|
82
142
|
return { method: key.slice(0, spaceIdx), route: key.slice(spaceIdx + 1) };
|
|
83
143
|
}
|
|
84
144
|
|
|
85
|
-
|
|
145
|
+
// The matching mock together with the KEY that declared it, or null. The key is
|
|
146
|
+
// what the usage tally counts — reporting "which declared mock fired" needs the
|
|
147
|
+
// declaration's identity, not just its body, since two keys can carry
|
|
148
|
+
// structurally identical mocks.
|
|
149
|
+
function findHttpMockEntry(httpMocks, request) {
|
|
86
150
|
const method = request.method().toUpperCase();
|
|
87
151
|
const url = request.url();
|
|
88
152
|
for (const [key, mock] of Object.entries(httpMocks)) {
|
|
@@ -90,11 +154,16 @@ function findHttpMock(httpMocks, request) {
|
|
|
90
154
|
if (!parsed) continue;
|
|
91
155
|
// Method stays an exact check; only the route is matched by pattern.
|
|
92
156
|
if (parsed.method.toUpperCase() !== method) continue;
|
|
93
|
-
if (matchUrl(parsed.route, url)) return mock;
|
|
157
|
+
if (matchUrl(parsed.route, url)) return { key, mock };
|
|
94
158
|
}
|
|
95
159
|
return null;
|
|
96
160
|
}
|
|
97
161
|
|
|
162
|
+
function findHttpMock(httpMocks, request) {
|
|
163
|
+
const entry = findHttpMockEntry(httpMocks, request);
|
|
164
|
+
return entry ? entry.mock : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
98
167
|
// The set of path/URL route patterns declared by the mock keys. A key is
|
|
99
168
|
// `"<METHOD> <route>"` (e.g. `"GET /api/plans"`); we strip the method so the
|
|
100
169
|
// route matcher can decide whether a request *could* match any mock without
|
|
@@ -121,8 +190,134 @@ function requestTargetsMock(targets, url) {
|
|
|
121
190
|
return false;
|
|
122
191
|
}
|
|
123
192
|
|
|
124
|
-
|
|
125
|
-
|
|
193
|
+
// The origin half of a URL, or the raw string when it will not parse. Grouping
|
|
194
|
+
// by origin is what turns twenty tile URLs into one readable line.
|
|
195
|
+
function originOf(url) {
|
|
196
|
+
try {
|
|
197
|
+
return new URL(url).origin;
|
|
198
|
+
} catch {
|
|
199
|
+
return url;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Which declared mock keys actually served a response.
|
|
204
|
+
//
|
|
205
|
+
// This exists because a declared mock whose key never matches any request is
|
|
206
|
+
// silently inert: the capture still reports success, and the frame that lands
|
|
207
|
+
// is the healthy state under a name promising a degraded one. The declared keys
|
|
208
|
+
// were always known; nothing ever compared them against what fired.
|
|
209
|
+
function createMockUsageTally(httpMocks) {
|
|
210
|
+
const declared = Object.keys(httpMocks || {});
|
|
211
|
+
const usedKeys = new Set();
|
|
212
|
+
return {
|
|
213
|
+
recordFulfilled(key) {
|
|
214
|
+
usedKeys.add(key);
|
|
215
|
+
},
|
|
216
|
+
// Declared order is preserved in both lists so the reported set reads the
|
|
217
|
+
// same way the scenario's mock block does.
|
|
218
|
+
get used() {
|
|
219
|
+
return declared.filter((key) => usedKeys.has(key));
|
|
220
|
+
},
|
|
221
|
+
get unused() {
|
|
222
|
+
return declared.filter((key) => !usedKeys.has(key));
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Every request the page made, collapsed to one row per `(origin, mocked)` pair.
|
|
228
|
+
// A full URL list is noise (twenty tile URLs); origin + count + a representative
|
|
229
|
+
// URL is the line that ends a "why is this page blank" misdiagnosis.
|
|
230
|
+
function createRequestInventory(appOrigin) {
|
|
231
|
+
// Keyed by `origin|mocked` rather than origin alone: an origin serving both a
|
|
232
|
+
// mocked API call and unmocked sub-resources would otherwise collapse into one
|
|
233
|
+
// row whose `mocked` flag is a coin toss.
|
|
234
|
+
const groups = new Map();
|
|
235
|
+
|
|
236
|
+
function groupFor(url, mocked) {
|
|
237
|
+
const origin = originOf(url);
|
|
238
|
+
const groupKey = `${origin}|${mocked ? "mocked" : "unmocked"}`;
|
|
239
|
+
let row = groups.get(groupKey);
|
|
240
|
+
if (!row) {
|
|
241
|
+
row = {
|
|
242
|
+
origin,
|
|
243
|
+
count: 0,
|
|
244
|
+
sampleUrl: url,
|
|
245
|
+
mocked: !!mocked,
|
|
246
|
+
failed: false,
|
|
247
|
+
// `false` when the capture URL did not parse — the caller then has no
|
|
248
|
+
// origin to compare against, and treating everything as cross-origin
|
|
249
|
+
// over-reports rather than hiding a request.
|
|
250
|
+
sameOrigin: !!appOrigin && origin === appOrigin,
|
|
251
|
+
};
|
|
252
|
+
groups.set(groupKey, row);
|
|
253
|
+
}
|
|
254
|
+
return row;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
recordRequest(url, mocked) {
|
|
259
|
+
groupFor(url, mocked).count += 1;
|
|
260
|
+
},
|
|
261
|
+
recordFailure(url, mocked) {
|
|
262
|
+
groupFor(url, mocked).failed = true;
|
|
263
|
+
},
|
|
264
|
+
// Busiest origin first — the one worth explaining leads the report.
|
|
265
|
+
get groups() {
|
|
266
|
+
return [...groups.values()].sort(
|
|
267
|
+
(a, b) => b.count - a.count || a.origin.localeCompare(b.origin),
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Composes the two independent halves above into the single handle the capture
|
|
274
|
+
// harness threads through.
|
|
275
|
+
//
|
|
276
|
+
// Purely observational. Nothing here changes which requests are intercepted:
|
|
277
|
+
// `attachHttpMocks` scopes `page.route` to declared targets so unmocked requests
|
|
278
|
+
// load natively, and that scoping is load-bearing (a blanket catch-all route
|
|
279
|
+
// predicate breaks Vite ESM module loading and blanks the SPA).
|
|
280
|
+
function createMockObserver(httpMocks, appOrigin) {
|
|
281
|
+
const usage = createMockUsageTally(httpMocks);
|
|
282
|
+
const inventory = createRequestInventory(appOrigin);
|
|
283
|
+
return {
|
|
284
|
+
recordFulfilled: (key) => usage.recordFulfilled(key),
|
|
285
|
+
recordRequest: (url, mocked) => inventory.recordRequest(url, mocked),
|
|
286
|
+
recordFailure: (url, mocked) => inventory.recordFailure(url, mocked),
|
|
287
|
+
get used() {
|
|
288
|
+
return usage.used;
|
|
289
|
+
},
|
|
290
|
+
get unused() {
|
|
291
|
+
return usage.unused;
|
|
292
|
+
},
|
|
293
|
+
get externalRequests() {
|
|
294
|
+
return inventory.groups;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Passive request inventory. `page.on` observes; it never routes, so attaching
|
|
300
|
+
// this cannot change what the page loads.
|
|
301
|
+
function attachRequestInventory(page, observer, httpMocks) {
|
|
302
|
+
const targets = mockedTargets(httpMocks || {});
|
|
303
|
+
page.on("request", (request) => {
|
|
304
|
+
const url = request.url();
|
|
305
|
+
observer.recordRequest(url, requestTargetsMock(targets, url));
|
|
306
|
+
});
|
|
307
|
+
page.on("requestfailed", (request) => {
|
|
308
|
+
const url = request.url();
|
|
309
|
+
observer.recordFailure(url, requestTargetsMock(targets, url));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Returns the capture's mock observer (see `createMockObserver`). Always returns
|
|
314
|
+
// one, including for a scenario that declares no mocks at all — that is exactly
|
|
315
|
+
// the case where an unexplained third-party fetch is hardest to account for.
|
|
316
|
+
async function attachHttpMocks(page, httpMocks, options = {}) {
|
|
317
|
+
const observer = createMockObserver(httpMocks, options.appOrigin);
|
|
318
|
+
attachRequestInventory(page, observer, httpMocks);
|
|
319
|
+
|
|
320
|
+
if (!httpMocks || Object.keys(httpMocks).length === 0) return observer;
|
|
126
321
|
|
|
127
322
|
// Intercept ONLY requests whose path matches a declared mock — not every
|
|
128
323
|
// request. A blanket `page.route("**/*")` intercepts the dev server's ESM
|
|
@@ -134,11 +329,15 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
134
329
|
await page.route(
|
|
135
330
|
(url) => requestTargetsMock(targets, url),
|
|
136
331
|
async (route) => {
|
|
137
|
-
const
|
|
138
|
-
if (!
|
|
332
|
+
const entry = findHttpMockEntry(httpMocks, route.request());
|
|
333
|
+
if (!entry) {
|
|
139
334
|
await route.continue();
|
|
140
335
|
return;
|
|
141
336
|
}
|
|
337
|
+
const { key, mock } = entry;
|
|
338
|
+
// Counted at the fulfil branch, not at match time: a key is "used" only
|
|
339
|
+
// when it actually served a response.
|
|
340
|
+
observer.recordFulfilled(key);
|
|
142
341
|
|
|
143
342
|
const headers = { ...(mock.headers || {}) };
|
|
144
343
|
let body;
|
|
@@ -175,6 +374,8 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
175
374
|
body: "[]",
|
|
176
375
|
});
|
|
177
376
|
});
|
|
377
|
+
|
|
378
|
+
return observer;
|
|
178
379
|
}
|
|
179
380
|
|
|
180
381
|
|
|
@@ -222,8 +423,13 @@ module.exports = {
|
|
|
222
423
|
matchPath,
|
|
223
424
|
matchQuerySpec,
|
|
224
425
|
findHttpMock,
|
|
426
|
+
findHttpMockEntry,
|
|
225
427
|
mockedTargets,
|
|
226
428
|
requestTargetsMock,
|
|
429
|
+
originOf,
|
|
430
|
+
createMockUsageTally,
|
|
431
|
+
createRequestInventory,
|
|
432
|
+
createMockObserver,
|
|
227
433
|
attachHttpMocks,
|
|
228
434
|
isDeclaredErrorMock,
|
|
229
435
|
unmockedRouteFrom,
|
|
@@ -146,20 +146,54 @@ function defaultReadServerState() {
|
|
|
146
146
|
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// Resolve the
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
149
|
+
// Resolve the loopback origin that serves the iframe-harness route. The harness
|
|
150
|
+
// is mounted on the editor's control-api listener, whose port is recorded in
|
|
151
|
+
// `.codeyam/server-state.json` as `controlPort` (the same file the top-level
|
|
152
|
+
// loader already reads for `appPort`). Returns `http://127.0.0.1:<controlPort>`
|
|
153
|
+
// — a secure context the nested iframe inherits, exactly as `localhost` is — or
|
|
154
|
+
// null when the state file is missing/unreadable, in which case the caller falls
|
|
155
|
+
// back to the legacy in-page `setContent` harness. `readStateFile` is injectable
|
|
156
|
+
// so the resolver is unit-testable without disk.
|
|
157
|
+
//
|
|
158
|
+
// The HOST is taken from `targetUrl` — the URL the iframe will load — and only
|
|
159
|
+
// the port comes from server-state. That mirroring is load-bearing, because the
|
|
160
|
+
// two capture modes address the editor by different loopback spellings:
|
|
161
|
+
// `/__codeyam_preview` captures are pinned to `127.0.0.1` (PROXY_CAPTURE_LOOPBACK
|
|
162
|
+
// in handlers.rs, matching the forwarder's own pinning) while direct app-port
|
|
163
|
+
// captures use `localhost`. `localhost` and `127.0.0.1` are DIFFERENT sites to
|
|
164
|
+
// the cookie jar, so whenever the harness host and the iframe host disagree the
|
|
165
|
+
// nested load is cross-site — and the `cy_session` cookie is `SameSite=Lax`,
|
|
166
|
+
// which rides top-level navigations only. It is therefore withheld from the
|
|
167
|
+
// iframe request (and from the `/api/*` calls the framed page makes), and the
|
|
168
|
+
// token-gated routes answer 401 on a non-loopback bind.
|
|
169
|
+
//
|
|
170
|
+
// Hardcoding EITHER spelling only moves the failure between the two modes:
|
|
171
|
+
// `localhost` 401s every `/__codeyam_preview` capture, `127.0.0.1` 401s the
|
|
172
|
+
// framed page's own `/api/scenarios` + `/api/render-environment` calls. Raising
|
|
173
|
+
// the cookie to `SameSite=None` fixes neither — Chromium rejects `None` without
|
|
174
|
+
// `Secure`, and these origins are plain http. Mirroring the target's host is what
|
|
175
|
+
// keeps the harness same-site in both modes, which is also the same-origin model
|
|
176
|
+
// the `/__codeyam_preview` subpath proxy exists to provide.
|
|
177
|
+
//
|
|
178
|
+
// `targetUrl` omitted or unparseable falls back to `127.0.0.1`, the spelling the
|
|
179
|
+
// proxy-route capture (the token-gated one) uses.
|
|
180
|
+
function resolveHarnessOrigin({
|
|
181
|
+
readStateFile = defaultReadServerState,
|
|
182
|
+
targetUrl = null,
|
|
183
|
+
} = {}) {
|
|
158
184
|
try {
|
|
159
185
|
const state = readStateFile();
|
|
160
186
|
const port = state && state.controlPort;
|
|
161
187
|
if (typeof port === "number" && port > 0) {
|
|
162
|
-
|
|
188
|
+
let host = "127.0.0.1";
|
|
189
|
+
if (targetUrl) {
|
|
190
|
+
try {
|
|
191
|
+
host = new URL(targetUrl).hostname || host;
|
|
192
|
+
} catch (_) {
|
|
193
|
+
/* unparseable target — keep the proxy-route default */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return `http://${host}:${port}`;
|
|
163
197
|
}
|
|
164
198
|
} catch (_) {
|
|
165
199
|
/* missing/unreadable state — fall back to setContent */
|
|
@@ -300,16 +334,36 @@ async function collectVisibleTextLength(target) {
|
|
|
300
334
|
}
|
|
301
335
|
|
|
302
336
|
// Inject a capture-only stylesheet that snaps entrance animations to their
|
|
303
|
-
// FINAL frame: remove animation/transition timing
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// interactive
|
|
311
|
-
//
|
|
312
|
-
//
|
|
337
|
+
// FINAL frame: remove animation/transition timing, then reveal the elements an
|
|
338
|
+
// entrance animation left INVISIBLE. This is the belt to
|
|
339
|
+
// `scrollThroughDocument`'s suspenders — it covers pure-CSS keyframe entrances
|
|
340
|
+
// that are mid-flight or stuck at an `opacity:0` start state even after the
|
|
341
|
+
// observers fired. It targets the generic CSS symptom, not any framework's
|
|
342
|
+
// reveal class, so it works for any stack. The caller gates this OFF when a
|
|
343
|
+
// scenario declares an interactive state, so an intentionally animated /
|
|
344
|
+
// collapsed interactive frame is never clobbered. Idempotent (a single injected
|
|
345
|
+
// style id) and best-effort. Returns true when the style is present after the
|
|
346
|
+
// call.
|
|
347
|
+
//
|
|
348
|
+
// The reveal is per-element and conditional ON THE ELEMENT BEING INVISIBLE,
|
|
349
|
+
// never a blanket `opacity: 1 !important` / `transform: none !important` over
|
|
350
|
+
// `*`. A blanket rule cannot tell an entrance animation's `opacity: 0` from
|
|
351
|
+
// DELIBERATE, resting state — a disabled control's dim, a muted row, a
|
|
352
|
+
// collapsed chevron's rotation — so it silently flattened every one of them out
|
|
353
|
+
// of every screenshot the capture pipeline produced. Two scenarios differing
|
|
354
|
+
// only in such a state then captured byte-identically and collided in the
|
|
355
|
+
// distinct-capture gate, and the collision was unfixable in the component: the
|
|
356
|
+
// state rendered correctly in a real browser (verified: computed opacity 0.4 vs
|
|
357
|
+
// 1) and was erased only at capture time. Anything already visible is now left
|
|
358
|
+
// exactly as the app rendered it.
|
|
359
|
+
//
|
|
360
|
+
// The reveal's TRANSFORM half additionally stops at the SVG boundary, because a
|
|
361
|
+
// CSS `transform` overrides the SVG `transform` presentation attribute: inside
|
|
362
|
+
// an `<svg>` it erases the static geometry that CONSTRUCTS the drawing instead
|
|
363
|
+
// of neutralizing an entrance animation, so a revealed SVG node lands on the
|
|
364
|
+
// origin. Invisible SVG nodes are therefore revealed in place — opacity forced,
|
|
365
|
+
// transform left alone. See the loop below for why the boundary is
|
|
366
|
+
// `ownerSVGElement` rather than `closest("svg")`.
|
|
313
367
|
async function forceFinalVisualState(target) {
|
|
314
368
|
return target.evaluate(() => {
|
|
315
369
|
const STYLE_ID = "__codeyam_force_final_state";
|
|
@@ -322,16 +376,54 @@ async function forceFinalVisualState(target) {
|
|
|
322
376
|
if (typeof document.createElement !== "function") return false;
|
|
323
377
|
const style = document.createElement("style");
|
|
324
378
|
style.id = STYLE_ID;
|
|
379
|
+
// Temporal properties only — these carry no resting state, so removing
|
|
380
|
+
// them cannot erase anything the app meant to show.
|
|
325
381
|
style.textContent =
|
|
326
382
|
"*, *::before, *::after {" +
|
|
327
383
|
"animation: none !important;" +
|
|
328
384
|
"transition: none !important;" +
|
|
329
|
-
"opacity: 1 !important;" +
|
|
330
|
-
"transform: none !important;" +
|
|
331
385
|
"}";
|
|
332
386
|
const head = document.head || document.documentElement;
|
|
333
387
|
if (!head || typeof head.appendChild !== "function") return false;
|
|
334
388
|
head.appendChild(style);
|
|
389
|
+
|
|
390
|
+
// With animations disabled above, an element held back by an entrance
|
|
391
|
+
// animation now computes to its pre-animation resting state — typically
|
|
392
|
+
// `opacity: 0`, often paired with a translate/scale that parks it offscreen.
|
|
393
|
+
// Reveal exactly those, and only those: a fully transparent element shows
|
|
394
|
+
// nothing either way, so forcing it can hide no real state, while an element
|
|
395
|
+
// at any visible opacity is left untouched.
|
|
396
|
+
//
|
|
397
|
+
// The transform half of the reveal STOPS at the SVG boundary. A CSS
|
|
398
|
+
// `transform` overrides the SVG `transform` presentation attribute, so
|
|
399
|
+
// inside an `<svg>` a forced `transform: none` does not neutralize an
|
|
400
|
+
// entrance animation — it erases the static rotate/translate/scale that
|
|
401
|
+
// CONSTRUCTS the drawing, revealing the node collapsed on the origin. The
|
|
402
|
+
// opacity half is still right there (a node at ~0 shows nothing either
|
|
403
|
+
// way), so an invisible SVG node is revealed IN PLACE. The boundary test is
|
|
404
|
+
// `ownerSVGElement` and NOT `closest("svg")`: HTML inside a
|
|
405
|
+
// `<foreignObject>` is a real HTMLElement with no `ownerSVGElement`, so it
|
|
406
|
+
// keeps the full reveal — `closest("svg")` would silently strand its
|
|
407
|
+
// genuine CSS entrance transform.
|
|
408
|
+
if (typeof document.querySelectorAll !== "function") return true;
|
|
409
|
+
const INVISIBLE_EPSILON = 0.01;
|
|
410
|
+
for (const el of document.querySelectorAll("*")) {
|
|
411
|
+
let computed = null;
|
|
412
|
+
try {
|
|
413
|
+
computed = getComputedStyle(el);
|
|
414
|
+
} catch (_) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (!computed) continue;
|
|
418
|
+
const opacity = parseFloat(computed.opacity);
|
|
419
|
+
if (!Number.isFinite(opacity) || opacity > INVISIBLE_EPSILON) continue;
|
|
420
|
+
if (!el.style || typeof el.style.setProperty !== "function") continue;
|
|
421
|
+
const inSvg =
|
|
422
|
+
el.ownerSVGElement != null ||
|
|
423
|
+
(typeof el.tagName === "string" && el.tagName.toLowerCase() === "svg");
|
|
424
|
+
el.style.setProperty("opacity", "1", "important");
|
|
425
|
+
if (!inSvg) el.style.setProperty("transform", "none", "important");
|
|
426
|
+
}
|
|
335
427
|
return true;
|
|
336
428
|
});
|
|
337
429
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeyam-editor/codeyam-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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
|
-
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.
|
|
14
|
-
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.
|
|
15
|
-
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.
|
|
12
|
+
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.8",
|
|
13
|
+
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.8",
|
|
14
|
+
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.8",
|
|
15
|
+
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.8"
|
|
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 O}from"./useEvents-S3Qk1Y_d.js";import{b as E}from"./index-
|
|
1
|
+
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as O}from"./useEvents-S3Qk1Y_d.js";import{b as E}from"./index-BPLKgwon.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}=O(),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 k({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:k,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(k,{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(k,{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}=O(),[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(k,{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}=O(),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}=O();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,k 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};
|