@reopt-ai/dev-proxy 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/README_KO.md +371 -0
- package/bin/dev-proxy.js +3 -0
- package/dist/bootstrap.js +3 -0
- package/dist/cli/config-io.js +110 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli.js +78 -0
- package/dist/commands/config.js +60 -0
- package/dist/commands/doctor.js +334 -0
- package/dist/commands/help.js +7 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/project.js +69 -0
- package/dist/commands/status.js +30 -0
- package/dist/commands/version.js +10 -0
- package/dist/commands/worktree.js +292 -0
- package/dist/components/app.js +394 -0
- package/dist/components/detail-panel.js +122 -0
- package/dist/components/footer-bar.js +62 -0
- package/dist/components/request-list.js +104 -0
- package/dist/components/splash.js +32 -0
- package/dist/components/status-bar.js +19 -0
- package/dist/hooks/use-mouse.js +66 -0
- package/dist/index.js +153 -0
- package/dist/proxy/certs.js +68 -0
- package/dist/proxy/config.js +78 -0
- package/dist/proxy/routes.js +70 -0
- package/dist/proxy/server.js +403 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy/worktrees.js +116 -0
- package/dist/store.js +567 -0
- package/dist/utils/format.js +121 -0
- package/dist/utils/list-layout.js +48 -0
- package/package.json +83 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
const MAX_EVENTS = 150;
|
|
3
|
+
const MAX_DETAIL = 50;
|
|
4
|
+
const DETAIL_IDLE_MS = 30_000; // 30s idle → stop collecting request detail
|
|
5
|
+
let events = [];
|
|
6
|
+
const activeWsIds = new Set();
|
|
7
|
+
const detailMap = new Map();
|
|
8
|
+
// O(1) index lookup by event id
|
|
9
|
+
const indexById = new Map();
|
|
10
|
+
let selectedIndex = -1;
|
|
11
|
+
let followMode = true;
|
|
12
|
+
let hideNextStatic = true;
|
|
13
|
+
let errorsOnly = false;
|
|
14
|
+
let searchQuery = "";
|
|
15
|
+
let version = 0;
|
|
16
|
+
let eventsRevision = 0;
|
|
17
|
+
let inspectActive = true;
|
|
18
|
+
// ── Detail idle management ───────────────────────────────
|
|
19
|
+
let detailActive = true;
|
|
20
|
+
let detailIdleTimer = null;
|
|
21
|
+
function resetDetailIdle() {
|
|
22
|
+
detailActive = true;
|
|
23
|
+
if (detailIdleTimer)
|
|
24
|
+
clearTimeout(detailIdleTimer);
|
|
25
|
+
detailIdleTimer = setTimeout(() => {
|
|
26
|
+
detailActive = false;
|
|
27
|
+
detailIdleTimer = null;
|
|
28
|
+
}, DETAIL_IDLE_MS);
|
|
29
|
+
}
|
|
30
|
+
export function isDetailActive() {
|
|
31
|
+
return detailActive;
|
|
32
|
+
}
|
|
33
|
+
// ── Noise filter ────────────────────────────────────────────
|
|
34
|
+
const NOISE_PREFIXES = ["/_next/", "/_next/webpack-hmr", "/__nextjs"];
|
|
35
|
+
const NOISE_RE = /^\/favicon[\w-]*\.\w+$/;
|
|
36
|
+
function isNoise(e) {
|
|
37
|
+
if (e.type === "ws")
|
|
38
|
+
return false;
|
|
39
|
+
const path = e.url.split("?")[0] ?? e.url;
|
|
40
|
+
return NOISE_PREFIXES.some((p) => path.startsWith(p)) || NOISE_RE.test(path);
|
|
41
|
+
}
|
|
42
|
+
function isError(e) {
|
|
43
|
+
if (e.type === "ws")
|
|
44
|
+
return e.wsStatus === "error";
|
|
45
|
+
return !!e.error || (e.statusCode !== undefined && e.statusCode >= 400);
|
|
46
|
+
}
|
|
47
|
+
// Cached filtered list — invalidated when any filter input changes
|
|
48
|
+
let cachedFiltered = null;
|
|
49
|
+
let _fRevision = -1;
|
|
50
|
+
let _fHide = null;
|
|
51
|
+
let _fErrors = null;
|
|
52
|
+
let _fSearch = null;
|
|
53
|
+
function filteredEvents() {
|
|
54
|
+
if (cachedFiltered &&
|
|
55
|
+
_fRevision === eventsRevision &&
|
|
56
|
+
_fHide === hideNextStatic &&
|
|
57
|
+
_fErrors === errorsOnly &&
|
|
58
|
+
_fSearch === searchQuery) {
|
|
59
|
+
return cachedFiltered;
|
|
60
|
+
}
|
|
61
|
+
_fRevision = eventsRevision;
|
|
62
|
+
_fHide = hideNextStatic;
|
|
63
|
+
_fErrors = errorsOnly;
|
|
64
|
+
_fSearch = searchQuery;
|
|
65
|
+
let result = events;
|
|
66
|
+
if (hideNextStatic)
|
|
67
|
+
result = result.filter((e) => !isNoise(e));
|
|
68
|
+
if (errorsOnly)
|
|
69
|
+
result = result.filter(isError);
|
|
70
|
+
if (searchQuery) {
|
|
71
|
+
const q = searchQuery.toLowerCase();
|
|
72
|
+
result = result.filter((e) => e.url.toLowerCase().includes(q) || e.method.toLowerCase().includes(q));
|
|
73
|
+
}
|
|
74
|
+
cachedFiltered = result;
|
|
75
|
+
return cachedFiltered;
|
|
76
|
+
}
|
|
77
|
+
const listeners = new Set();
|
|
78
|
+
const selectedListeners = new Set();
|
|
79
|
+
let selectedDirty = false;
|
|
80
|
+
// ── Throttled notify ─────────────────────────────────────────
|
|
81
|
+
// Cap re-renders to ~20 fps (50ms). Requests arriving across different
|
|
82
|
+
// event-loop ticks are coalesced into a single React render pass.
|
|
83
|
+
const RENDER_INTERVAL = 100; // ms (~10fps, sufficient for terminal UI)
|
|
84
|
+
let timer = null;
|
|
85
|
+
function notify() {
|
|
86
|
+
version++;
|
|
87
|
+
if (!inspectActive)
|
|
88
|
+
return;
|
|
89
|
+
timer ??= setTimeout(flush, RENDER_INTERVAL);
|
|
90
|
+
}
|
|
91
|
+
function notifySelected() {
|
|
92
|
+
for (const listener of selectedListeners) {
|
|
93
|
+
listener();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function flush() {
|
|
97
|
+
timer = null;
|
|
98
|
+
for (const listener of listeners) {
|
|
99
|
+
listener();
|
|
100
|
+
}
|
|
101
|
+
if (selectedDirty) {
|
|
102
|
+
selectedDirty = false;
|
|
103
|
+
notifySelected();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function splitEvent(event) {
|
|
107
|
+
const { cookies, query, requestHeaders, responseHeaders, ...slim } = event;
|
|
108
|
+
return { slim, detail: { cookies, query, requestHeaders, responseHeaders } };
|
|
109
|
+
}
|
|
110
|
+
function splitEventSlimOnly(event) {
|
|
111
|
+
const { cookies: _, query: _q, requestHeaders: _rh, responseHeaders: _rs, ...slim } = event;
|
|
112
|
+
return slim;
|
|
113
|
+
}
|
|
114
|
+
/** Evict oldest events when over capacity. Noise is evicted first, then oldest non-noise. */
|
|
115
|
+
function evictExcess() {
|
|
116
|
+
if (events.length <= MAX_EVENTS)
|
|
117
|
+
return;
|
|
118
|
+
const excess = events.length - MAX_EVENTS;
|
|
119
|
+
const removeIds = [];
|
|
120
|
+
for (const e of events) {
|
|
121
|
+
if (removeIds.length >= excess)
|
|
122
|
+
break;
|
|
123
|
+
if (isNoise(e))
|
|
124
|
+
removeIds.push(e.id);
|
|
125
|
+
}
|
|
126
|
+
if (removeIds.length < excess) {
|
|
127
|
+
const removeSet = new Set(removeIds);
|
|
128
|
+
for (const e of events) {
|
|
129
|
+
if (removeIds.length >= excess)
|
|
130
|
+
break;
|
|
131
|
+
if (!removeSet.has(e.id))
|
|
132
|
+
removeIds.push(e.id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const toRemove = new Set(removeIds);
|
|
136
|
+
for (const id of toRemove) {
|
|
137
|
+
detailMap.delete(id);
|
|
138
|
+
indexById.delete(id);
|
|
139
|
+
activeWsIds.delete(id);
|
|
140
|
+
}
|
|
141
|
+
// In-place compaction: avoid events.filter() + rebuild
|
|
142
|
+
let writeIdx = 0;
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of -- in-place compaction needs writeIdx
|
|
144
|
+
for (let i = 0; i < events.length; i++) {
|
|
145
|
+
const ev = events[i];
|
|
146
|
+
if (!toRemove.has(ev.id)) {
|
|
147
|
+
events[writeIdx] = ev;
|
|
148
|
+
indexById.set(ev.id, writeIdx);
|
|
149
|
+
writeIdx++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
events.length = writeIdx;
|
|
153
|
+
}
|
|
154
|
+
function shellQuote(value) {
|
|
155
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
156
|
+
}
|
|
157
|
+
export function pushHttp(event) {
|
|
158
|
+
eventsRevision++;
|
|
159
|
+
let slim;
|
|
160
|
+
let detail;
|
|
161
|
+
if (detailActive) {
|
|
162
|
+
const split = splitEvent(event);
|
|
163
|
+
slim = split.slim;
|
|
164
|
+
detail = split.detail;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
slim = splitEventSlimOnly(event);
|
|
168
|
+
detail = null;
|
|
169
|
+
}
|
|
170
|
+
const prevSelectedId = events[selectedIndex]?.id;
|
|
171
|
+
const existingIdx = indexById.get(slim.id);
|
|
172
|
+
if (existingIdx !== undefined) {
|
|
173
|
+
// Mutable in-place update — snapshot correctness handled by version bump
|
|
174
|
+
events[existingIdx] = slim;
|
|
175
|
+
cachedFiltered = null;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
events.push(slim);
|
|
179
|
+
indexById.set(slim.id, events.length - 1);
|
|
180
|
+
evictExcess();
|
|
181
|
+
if (followMode) {
|
|
182
|
+
const filtered = filteredEvents();
|
|
183
|
+
const last = filtered[filtered.length - 1];
|
|
184
|
+
if (last) {
|
|
185
|
+
selectedIndex = indexById.get(last.id) ?? -1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// LRU detail: skip entirely when idle (no user interaction)
|
|
190
|
+
if (detail) {
|
|
191
|
+
detailMap.delete(slim.id);
|
|
192
|
+
detailMap.set(slim.id, detail);
|
|
193
|
+
if (detailMap.size > MAX_DETAIL) {
|
|
194
|
+
const iter = detailMap.keys();
|
|
195
|
+
while (detailMap.size > MAX_DETAIL) {
|
|
196
|
+
const { value, done } = iter.next();
|
|
197
|
+
if (done)
|
|
198
|
+
break;
|
|
199
|
+
detailMap.delete(value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const nextSelected = events[selectedIndex];
|
|
204
|
+
const nextSelectedId = nextSelected?.id;
|
|
205
|
+
const selectedChanged = prevSelectedId !== nextSelectedId;
|
|
206
|
+
const selectedUpdated = prevSelectedId === slim.id;
|
|
207
|
+
const selectedEvicted = nextSelected?.type === "http" ? !detailMap.has(nextSelected.id) : false;
|
|
208
|
+
if (selectedChanged || selectedUpdated || selectedEvicted) {
|
|
209
|
+
selectedDirty = true;
|
|
210
|
+
}
|
|
211
|
+
notify();
|
|
212
|
+
}
|
|
213
|
+
export function pushWs(event) {
|
|
214
|
+
eventsRevision++;
|
|
215
|
+
const slimWs = {
|
|
216
|
+
id: event.id,
|
|
217
|
+
type: "ws",
|
|
218
|
+
protocol: event.protocol,
|
|
219
|
+
timestamp: event.timestamp,
|
|
220
|
+
method: "WS",
|
|
221
|
+
url: event.url,
|
|
222
|
+
host: event.host,
|
|
223
|
+
target: event.target,
|
|
224
|
+
wsStatus: event.status,
|
|
225
|
+
duration: event.duration,
|
|
226
|
+
error: event.error,
|
|
227
|
+
};
|
|
228
|
+
const prevSelectedId = events[selectedIndex]?.id;
|
|
229
|
+
// Update unified events list
|
|
230
|
+
const existingIdx = indexById.get(slimWs.id);
|
|
231
|
+
if (existingIdx !== undefined) {
|
|
232
|
+
// Mutable in-place update for close/error
|
|
233
|
+
events[existingIdx] = slimWs;
|
|
234
|
+
cachedFiltered = null;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
events.push(slimWs);
|
|
238
|
+
indexById.set(slimWs.id, events.length - 1);
|
|
239
|
+
evictExcess();
|
|
240
|
+
if (followMode) {
|
|
241
|
+
const filtered = filteredEvents();
|
|
242
|
+
const last = filtered[filtered.length - 1];
|
|
243
|
+
if (last) {
|
|
244
|
+
selectedIndex = indexById.get(last.id) ?? -1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (event.status === "open") {
|
|
249
|
+
activeWsIds.add(event.id);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
activeWsIds.delete(event.id);
|
|
253
|
+
}
|
|
254
|
+
const nextSelectedId = events[selectedIndex]?.id;
|
|
255
|
+
const selectedChanged = prevSelectedId !== nextSelectedId;
|
|
256
|
+
const selectedUpdated = prevSelectedId === slimWs.id;
|
|
257
|
+
if (selectedChanged || selectedUpdated) {
|
|
258
|
+
selectedDirty = true;
|
|
259
|
+
}
|
|
260
|
+
notify();
|
|
261
|
+
}
|
|
262
|
+
// Navigate within the filtered view, mapping back to raw index
|
|
263
|
+
function rawIndexOf(filtered, filteredIdx) {
|
|
264
|
+
const event = filtered[filteredIdx];
|
|
265
|
+
if (!event)
|
|
266
|
+
return -1;
|
|
267
|
+
return indexById.get(event.id) ?? -1;
|
|
268
|
+
}
|
|
269
|
+
// Synchronous flush for user interactions — no deferred rendering for keystrokes.
|
|
270
|
+
// Always notifies both main and selected listeners in one pass.
|
|
271
|
+
function notifySync() {
|
|
272
|
+
version++;
|
|
273
|
+
if (timer) {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
timer = null;
|
|
276
|
+
}
|
|
277
|
+
for (const listener of listeners) {
|
|
278
|
+
listener();
|
|
279
|
+
}
|
|
280
|
+
selectedDirty = false;
|
|
281
|
+
for (const listener of selectedListeners) {
|
|
282
|
+
listener();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export function setInspectActive(active) {
|
|
286
|
+
if (inspectActive === active)
|
|
287
|
+
return;
|
|
288
|
+
inspectActive = active;
|
|
289
|
+
if (!active) {
|
|
290
|
+
if (timer) {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
timer = null;
|
|
293
|
+
}
|
|
294
|
+
if (detailIdleTimer) {
|
|
295
|
+
clearTimeout(detailIdleTimer);
|
|
296
|
+
detailIdleTimer = null;
|
|
297
|
+
}
|
|
298
|
+
detailActive = false;
|
|
299
|
+
selectedDirty = false;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
resetDetailIdle();
|
|
303
|
+
notifySync();
|
|
304
|
+
}
|
|
305
|
+
export function selectNext() {
|
|
306
|
+
resetDetailIdle();
|
|
307
|
+
const filtered = filteredEvents();
|
|
308
|
+
if (filtered.length === 0)
|
|
309
|
+
return;
|
|
310
|
+
const current = events[selectedIndex];
|
|
311
|
+
const curFiltered = current ? filtered.findIndex((e) => e.id === current.id) : -1;
|
|
312
|
+
const nextFiltered = Math.min(curFiltered + 1, filtered.length - 1);
|
|
313
|
+
selectedIndex = rawIndexOf(filtered, nextFiltered);
|
|
314
|
+
followMode = false;
|
|
315
|
+
notifySync();
|
|
316
|
+
}
|
|
317
|
+
export function selectPrev() {
|
|
318
|
+
resetDetailIdle();
|
|
319
|
+
const filtered = filteredEvents();
|
|
320
|
+
if (filtered.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
const current = events[selectedIndex];
|
|
323
|
+
const curFiltered = current ? filtered.findIndex((e) => e.id === current.id) : 0;
|
|
324
|
+
const prevFiltered = Math.max(curFiltered - 1, 0);
|
|
325
|
+
selectedIndex = rawIndexOf(filtered, prevFiltered);
|
|
326
|
+
followMode = false;
|
|
327
|
+
notifySync();
|
|
328
|
+
}
|
|
329
|
+
export function selectFirst() {
|
|
330
|
+
resetDetailIdle();
|
|
331
|
+
const filtered = filteredEvents();
|
|
332
|
+
if (filtered.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
selectedIndex = rawIndexOf(filtered, 0);
|
|
335
|
+
followMode = false;
|
|
336
|
+
notifySync();
|
|
337
|
+
}
|
|
338
|
+
export function selectLast() {
|
|
339
|
+
resetDetailIdle();
|
|
340
|
+
const filtered = filteredEvents();
|
|
341
|
+
if (filtered.length === 0)
|
|
342
|
+
return;
|
|
343
|
+
selectedIndex = rawIndexOf(filtered, filtered.length - 1);
|
|
344
|
+
followMode = true;
|
|
345
|
+
notifySync();
|
|
346
|
+
}
|
|
347
|
+
export function selectByFilteredIndex(filteredIdx) {
|
|
348
|
+
resetDetailIdle();
|
|
349
|
+
const filtered = filteredEvents();
|
|
350
|
+
if (filtered.length === 0)
|
|
351
|
+
return;
|
|
352
|
+
if (filteredIdx < 0 || filteredIdx >= filtered.length)
|
|
353
|
+
return;
|
|
354
|
+
selectedIndex = rawIndexOf(filtered, filteredIdx);
|
|
355
|
+
followMode = false;
|
|
356
|
+
notifySync();
|
|
357
|
+
}
|
|
358
|
+
export function pauseFollow() {
|
|
359
|
+
if (!followMode)
|
|
360
|
+
return;
|
|
361
|
+
followMode = false;
|
|
362
|
+
notifySync();
|
|
363
|
+
}
|
|
364
|
+
export function toggleFollow() {
|
|
365
|
+
followMode = !followMode;
|
|
366
|
+
if (followMode) {
|
|
367
|
+
const filtered = filteredEvents();
|
|
368
|
+
if (filtered.length > 0) {
|
|
369
|
+
selectedIndex = rawIndexOf(filtered, filtered.length - 1);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
selectedIndex = -1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
notifySync();
|
|
376
|
+
}
|
|
377
|
+
export function toggleHideNoise() {
|
|
378
|
+
hideNextStatic = !hideNextStatic;
|
|
379
|
+
// Reset selection to avoid pointing at a now-invisible item
|
|
380
|
+
const filtered = filteredEvents();
|
|
381
|
+
if (filtered.length > 0) {
|
|
382
|
+
selectedIndex = rawIndexOf(filtered, filtered.length - 1);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
selectedIndex = -1;
|
|
386
|
+
}
|
|
387
|
+
notifySync();
|
|
388
|
+
}
|
|
389
|
+
export function toggleErrorsOnly() {
|
|
390
|
+
errorsOnly = !errorsOnly;
|
|
391
|
+
const filtered = filteredEvents();
|
|
392
|
+
if (filtered.length > 0) {
|
|
393
|
+
selectedIndex = rawIndexOf(filtered, filtered.length - 1);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
selectedIndex = -1;
|
|
397
|
+
}
|
|
398
|
+
notifySync();
|
|
399
|
+
}
|
|
400
|
+
export function setSearchQuery(q) {
|
|
401
|
+
searchQuery = q;
|
|
402
|
+
cachedFiltered = null;
|
|
403
|
+
const filtered = filteredEvents();
|
|
404
|
+
if (filtered.length > 0) {
|
|
405
|
+
selectedIndex = rawIndexOf(filtered, filtered.length - 1);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
selectedIndex = -1;
|
|
409
|
+
}
|
|
410
|
+
notifySync();
|
|
411
|
+
}
|
|
412
|
+
export function getSearchQuery() {
|
|
413
|
+
return searchQuery;
|
|
414
|
+
}
|
|
415
|
+
export function clearAll() {
|
|
416
|
+
events = [];
|
|
417
|
+
activeWsIds.clear();
|
|
418
|
+
detailMap.clear();
|
|
419
|
+
indexById.clear();
|
|
420
|
+
cachedFiltered = null;
|
|
421
|
+
eventsRevision = 0;
|
|
422
|
+
selectedIndex = -1;
|
|
423
|
+
followMode = true;
|
|
424
|
+
hideNextStatic = true;
|
|
425
|
+
errorsOnly = false;
|
|
426
|
+
searchQuery = "";
|
|
427
|
+
if (detailIdleTimer) {
|
|
428
|
+
clearTimeout(detailIdleTimer);
|
|
429
|
+
detailIdleTimer = null;
|
|
430
|
+
}
|
|
431
|
+
detailActive = true;
|
|
432
|
+
notifySync();
|
|
433
|
+
}
|
|
434
|
+
let cachedSnapshot = null;
|
|
435
|
+
let cachedVersion = -1;
|
|
436
|
+
function getSnapshot() {
|
|
437
|
+
if (cachedVersion !== version) {
|
|
438
|
+
const filtered = filteredEvents();
|
|
439
|
+
// Map raw selectedIndex to filtered index (same object refs → indexOf works)
|
|
440
|
+
const selectedEvent = events[selectedIndex];
|
|
441
|
+
const filteredIdx = selectedEvent ? filtered.indexOf(selectedEvent) : -1;
|
|
442
|
+
cachedSnapshot = {
|
|
443
|
+
events: filtered,
|
|
444
|
+
selectedIndex: filteredIdx,
|
|
445
|
+
followMode,
|
|
446
|
+
hideNextStatic,
|
|
447
|
+
errorsOnly,
|
|
448
|
+
searchQuery,
|
|
449
|
+
activeWsCount: activeWsIds.size,
|
|
450
|
+
version,
|
|
451
|
+
};
|
|
452
|
+
cachedVersion = version;
|
|
453
|
+
}
|
|
454
|
+
return cachedSnapshot;
|
|
455
|
+
}
|
|
456
|
+
function subscribe(callback) {
|
|
457
|
+
listeners.add(callback);
|
|
458
|
+
return () => {
|
|
459
|
+
listeners.delete(callback);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function subscribeSelected(callback) {
|
|
463
|
+
selectedListeners.add(callback);
|
|
464
|
+
return () => {
|
|
465
|
+
selectedListeners.delete(callback);
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
export function useStore() {
|
|
469
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
470
|
+
}
|
|
471
|
+
export function useSelected() {
|
|
472
|
+
const getSelected = () => events[selectedIndex];
|
|
473
|
+
return useSyncExternalStore(subscribeSelected, getSelected, getSelected);
|
|
474
|
+
}
|
|
475
|
+
export function useSelectedDetail() {
|
|
476
|
+
const getDetail = () => {
|
|
477
|
+
const event = events[selectedIndex];
|
|
478
|
+
if (!event)
|
|
479
|
+
return null;
|
|
480
|
+
return detailMap.get(event.id) ?? null;
|
|
481
|
+
};
|
|
482
|
+
return useSyncExternalStore(subscribeSelected, getDetail, getDetail);
|
|
483
|
+
}
|
|
484
|
+
export function useActiveWsCount() {
|
|
485
|
+
const snap = useStore();
|
|
486
|
+
return snap.activeWsCount;
|
|
487
|
+
}
|
|
488
|
+
// ── Replay ───────────────────────────────────────────────────
|
|
489
|
+
export function getSelectedReplayInfo() {
|
|
490
|
+
const event = events[selectedIndex];
|
|
491
|
+
if (!event || event.type === "ws")
|
|
492
|
+
return null;
|
|
493
|
+
const detail = detailMap.get(event.id);
|
|
494
|
+
return {
|
|
495
|
+
method: event.method,
|
|
496
|
+
protocol: event.protocol,
|
|
497
|
+
url: event.url,
|
|
498
|
+
host: event.host,
|
|
499
|
+
requestHeaders: detail?.requestHeaders ?? {},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// ── Curl export ──────────────────────────────────────────────
|
|
503
|
+
export function getSelectedCurl() {
|
|
504
|
+
const event = events[selectedIndex];
|
|
505
|
+
if (!event || event.type === "ws")
|
|
506
|
+
return null;
|
|
507
|
+
const detail = detailMap.get(event.id);
|
|
508
|
+
const parts = ["curl"];
|
|
509
|
+
if (event.method !== "GET")
|
|
510
|
+
parts.push(`-X ${event.method}`);
|
|
511
|
+
parts.push(shellQuote(`${event.protocol}://${event.host}${event.url}`));
|
|
512
|
+
if (detail) {
|
|
513
|
+
for (const [key, val] of Object.entries(detail.requestHeaders)) {
|
|
514
|
+
if (key === "host")
|
|
515
|
+
continue;
|
|
516
|
+
const v = Array.isArray(val) ? val.join(", ") : val;
|
|
517
|
+
parts.push(`-H ${shellQuote(`${key}: ${v}`)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return parts.join(" \\\n ");
|
|
521
|
+
}
|
|
522
|
+
export const __testing = {
|
|
523
|
+
reset() {
|
|
524
|
+
events = [];
|
|
525
|
+
activeWsIds.clear();
|
|
526
|
+
detailMap.clear();
|
|
527
|
+
indexById.clear();
|
|
528
|
+
selectedIndex = -1;
|
|
529
|
+
followMode = true;
|
|
530
|
+
hideNextStatic = true;
|
|
531
|
+
errorsOnly = false;
|
|
532
|
+
searchQuery = "";
|
|
533
|
+
version = 0;
|
|
534
|
+
eventsRevision = 0;
|
|
535
|
+
inspectActive = true;
|
|
536
|
+
cachedFiltered = null;
|
|
537
|
+
_fRevision = -1;
|
|
538
|
+
_fHide = null;
|
|
539
|
+
_fErrors = null;
|
|
540
|
+
_fSearch = null;
|
|
541
|
+
cachedSnapshot = null;
|
|
542
|
+
cachedVersion = -1;
|
|
543
|
+
selectedDirty = false;
|
|
544
|
+
if (timer) {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
timer = null;
|
|
547
|
+
}
|
|
548
|
+
if (detailIdleTimer) {
|
|
549
|
+
clearTimeout(detailIdleTimer);
|
|
550
|
+
detailIdleTimer = null;
|
|
551
|
+
}
|
|
552
|
+
detailActive = true;
|
|
553
|
+
listeners.clear();
|
|
554
|
+
selectedListeners.clear();
|
|
555
|
+
},
|
|
556
|
+
snapshot: getSnapshot,
|
|
557
|
+
selected() {
|
|
558
|
+
return events[selectedIndex];
|
|
559
|
+
},
|
|
560
|
+
selectedDetail() {
|
|
561
|
+
const event = events[selectedIndex];
|
|
562
|
+
if (!event)
|
|
563
|
+
return null;
|
|
564
|
+
return detailMap.get(event.id) ?? null;
|
|
565
|
+
},
|
|
566
|
+
subscribeSelected,
|
|
567
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ── Color palette ────────────────────────────────────────────
|
|
2
|
+
export const palette = {
|
|
3
|
+
brand: "#CBA6F7", // mauve
|
|
4
|
+
accent: "#89DCEB", // sky
|
|
5
|
+
success: "#A6E3A1", // green
|
|
6
|
+
warning: "#F9E2AF", // yellow
|
|
7
|
+
error: "#F38BA8", // red
|
|
8
|
+
info: "#89B4FA", // blue
|
|
9
|
+
muted: "#7F849C", // overlay1
|
|
10
|
+
subtle: "#45475A", // surface1
|
|
11
|
+
surface: "#1E1E2E", // base
|
|
12
|
+
text: "#CDD6F4", // text
|
|
13
|
+
dim: "#A6ADC8", // subtext0
|
|
14
|
+
ws: "#B4BEFE", // lavender
|
|
15
|
+
selection: "#313244", // surface0
|
|
16
|
+
border: "#585B70", // surface2
|
|
17
|
+
};
|
|
18
|
+
// ── Time formatting ─────────────────────────────────────────
|
|
19
|
+
export function formatTime(ts) {
|
|
20
|
+
const d = new Date(ts);
|
|
21
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
22
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
23
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
24
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
25
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
26
|
+
return `${mm}-${dd} ${h}:${m}:${s}`;
|
|
27
|
+
}
|
|
28
|
+
export function formatDuration(ms) {
|
|
29
|
+
if (ms === undefined)
|
|
30
|
+
return "\u2022\u2022\u2022";
|
|
31
|
+
if (ms < 1000)
|
|
32
|
+
return `${ms}ms`;
|
|
33
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
+
}
|
|
35
|
+
export function formatSize(bytes) {
|
|
36
|
+
if (bytes === undefined)
|
|
37
|
+
return "\u2022\u2022\u2022";
|
|
38
|
+
if (bytes < 1024)
|
|
39
|
+
return `${bytes}B`;
|
|
40
|
+
if (bytes < 1024 * 1024)
|
|
41
|
+
return `${(bytes / 1024).toFixed(1)}K`;
|
|
42
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
43
|
+
}
|
|
44
|
+
// ── Method colors ───────────────────────────────────────────
|
|
45
|
+
export function methodColor(method) {
|
|
46
|
+
switch (method.toUpperCase()) {
|
|
47
|
+
case "GET":
|
|
48
|
+
return palette.success;
|
|
49
|
+
case "POST":
|
|
50
|
+
return palette.warning;
|
|
51
|
+
case "PUT":
|
|
52
|
+
return palette.info;
|
|
53
|
+
case "PATCH":
|
|
54
|
+
return palette.info;
|
|
55
|
+
case "DELETE":
|
|
56
|
+
return palette.error;
|
|
57
|
+
case "HEAD":
|
|
58
|
+
return palette.dim;
|
|
59
|
+
case "OPTIONS":
|
|
60
|
+
return palette.dim;
|
|
61
|
+
case "WS":
|
|
62
|
+
return palette.ws;
|
|
63
|
+
default:
|
|
64
|
+
return palette.text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ── Status colors ───────────────────────────────────────────
|
|
68
|
+
export function statusColor(code) {
|
|
69
|
+
if (code === undefined)
|
|
70
|
+
return palette.muted;
|
|
71
|
+
if (code < 300)
|
|
72
|
+
return palette.success;
|
|
73
|
+
if (code < 400)
|
|
74
|
+
return palette.accent;
|
|
75
|
+
if (code < 500)
|
|
76
|
+
return palette.warning;
|
|
77
|
+
return palette.error;
|
|
78
|
+
}
|
|
79
|
+
// ── Duration colors ─────────────────────────────────────────
|
|
80
|
+
export function durationColor(ms) {
|
|
81
|
+
if (ms === undefined)
|
|
82
|
+
return palette.muted;
|
|
83
|
+
if (ms >= 2000)
|
|
84
|
+
return palette.error;
|
|
85
|
+
if (ms >= 500)
|
|
86
|
+
return palette.warning;
|
|
87
|
+
return palette.dim;
|
|
88
|
+
}
|
|
89
|
+
// ── URL formatting ──────────────────────────────────────────
|
|
90
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
91
|
+
export function compactUrl(url) {
|
|
92
|
+
return url.replace(UUID_RE, "{id}");
|
|
93
|
+
}
|
|
94
|
+
// ── String utils ────────────────────────────────────────────
|
|
95
|
+
export function truncate(str, maxLen) {
|
|
96
|
+
if (str.length <= maxLen)
|
|
97
|
+
return str;
|
|
98
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
99
|
+
}
|
|
100
|
+
export function pad(str, len) {
|
|
101
|
+
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
102
|
+
}
|
|
103
|
+
// ── Subdomain colors ────────────────────────────────────────
|
|
104
|
+
const subColors = {
|
|
105
|
+
studio: "#A78BFA", // violet
|
|
106
|
+
www: "#4ADE80", // green
|
|
107
|
+
account: "#FBBF24", // amber
|
|
108
|
+
data: "#22D3EE", // cyan
|
|
109
|
+
qa: "#F87171", // red
|
|
110
|
+
ops: "#FB923C", // orange
|
|
111
|
+
handbook: "#34D399", // emerald
|
|
112
|
+
kb: "#818CF8", // indigo
|
|
113
|
+
docs: "#60A5FA", // blue
|
|
114
|
+
angski: "#F472B6", // pink
|
|
115
|
+
};
|
|
116
|
+
export function subdomainFrom(host) {
|
|
117
|
+
return host.split(".")[0] ?? "*";
|
|
118
|
+
}
|
|
119
|
+
export function subdomainColor(sub) {
|
|
120
|
+
return subColors[sub] ?? palette.dim;
|
|
121
|
+
}
|