@zerotal/devtools 1.6.3 → 1.7.2
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/CHANGELOG.md +329 -0
- package/api-surface.md +298 -0
- package/package.json +5 -4
- package/src/DevtoolsInjectionMiddleware.ts +41 -4
- package/src/RequestTrace.ts +106 -1
- package/src/TraceStore.ts +12 -0
- package/src/activity.ts +116 -0
- package/src/callsite.ts +146 -0
- package/src/client/filter.ts +108 -0
- package/src/client/index.ts +127 -0
- package/src/client/metrics.ts +98 -0
- package/src/client/registry.ts +87 -0
- package/src/client/state.ts +350 -0
- package/src/client/tabs/all.ts +323 -0
- package/src/client/tabs/app.ts +293 -0
- package/src/client/tabs/cache.ts +50 -0
- package/src/client/tabs/channel.ts +264 -0
- package/src/client/tabs/exceptions.ts +69 -0
- package/src/client/tabs/jobs.ts +51 -0
- package/src/client/tabs/live.ts +66 -0
- package/src/client/tabs/logs.ts +45 -0
- package/src/client/tabs/mail.ts +60 -0
- package/src/client/tabs/queries.ts +125 -0
- package/src/client/tabs/request.ts +75 -0
- package/src/client/tabs/sections.ts +115 -0
- package/src/client/tabs/timeline.ts +133 -0
- package/src/client/tabs/types.ts +68 -0
- package/src/client/transport.ts +81 -0
- package/src/client/tree.ts +138 -0
- package/src/client/ui/format.ts +137 -0
- package/src/client/ui/render.ts +87 -0
- package/src/client/ui/shell.ts +560 -0
- package/src/client/ui/theme.ts +445 -0
- package/src/client-auto.ts +1 -1
- package/src/config.ts +77 -2
- package/src/dashboard-auto.ts +1 -1
- package/src/editor.ts +107 -0
- package/src/enabled.ts +59 -0
- package/src/index.ts +19 -3
- package/src/map.ts +213 -0
- package/src/provider/DevtoolsProvider.ts +32 -7
- package/src/redaction.ts +161 -20
- package/src/tracing.ts +261 -29
- package/src/client.ts +0 -1048
- package/src/panel-app.js +0 -519
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every style the panel has, as one string injected into its shadow root.
|
|
3
|
+
*
|
|
4
|
+
* Two palettes, one set of rules. The tokens are declared twice — once on `#wrap`
|
|
5
|
+
* and once on `#wrap.light` — and a class decides which applies, rather than a
|
|
6
|
+
* media query deciding it in CSS. That keeps the "follow the system" case and the
|
|
7
|
+
* explicit override from fighting each other over specificity: the panel resolves
|
|
8
|
+
* the three-way choice (auto / light / dark) in one place in JavaScript and the
|
|
9
|
+
* stylesheet only ever sees the answer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The one place a colour is named. Every rule below reads these through `var()`. */
|
|
13
|
+
const TOKENS = `
|
|
14
|
+
#wrap {
|
|
15
|
+
--bg: #1a1b26; --surf: #24283b; --card: #2f3452; --bdr: #3b4261;
|
|
16
|
+
/* --muted carries labels, hints and every inactive tab. At the palette's own
|
|
17
|
+
#565f89 that is 2.35:1 on --surf — below AA for text of any size, and this
|
|
18
|
+
is 10–11px text. Lifted until it clears 4.5:1 while staying recessive. */
|
|
19
|
+
--text: #c0caf5; --muted: #8790bd; --purple: #7aa2f7;
|
|
20
|
+
--green: #9ece6a; --yellow: #e0af68; --red: #f7768e;
|
|
21
|
+
--cyan: #7dcfff; --orange: #ff9e64;
|
|
22
|
+
--childbg: rgba(0,0,0,.15);
|
|
23
|
+
}
|
|
24
|
+
#wrap.light {
|
|
25
|
+
--bg: #f4f4f8; --surf: #e8e9f0; --card: #dcdee8; --bdr: #c0c4d4;
|
|
26
|
+
--text: #343b58; --muted: #565c7d; --purple: #34548a;
|
|
27
|
+
--green: #33635c; --yellow: #8f5e15; --red: #c64343;
|
|
28
|
+
--cyan: #0f4b6e; --orange: #965027;
|
|
29
|
+
--childbg: rgba(0,0,0,.05);
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
export const CSS = `<style>
|
|
33
|
+
:host { all: initial; }
|
|
34
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
35
|
+
${TOKENS}
|
|
36
|
+
#wrap {
|
|
37
|
+
font: 12px/1.5 'JetBrains Mono','Fira Code','SF Mono',ui-monospace,monospace;
|
|
38
|
+
color: var(--text);
|
|
39
|
+
}
|
|
40
|
+
#wrap:focus { outline: none; }
|
|
41
|
+
/* The container itself takes focus on open and should not draw a ring for it.
|
|
42
|
+
Anything a person actually tabs to must — the panel used to suppress focus
|
|
43
|
+
everywhere, which left keyboard navigation invisible. */
|
|
44
|
+
#wrap :focus-visible {
|
|
45
|
+
outline: 2px solid var(--purple);
|
|
46
|
+
outline-offset: -2px;
|
|
47
|
+
border-radius: 2px;
|
|
48
|
+
}
|
|
49
|
+
/* Numbers line up only if the digits are the same width. Durations sit in a
|
|
50
|
+
right-aligned column, and proportional digits are what made its left edge
|
|
51
|
+
ragged from row to row. */
|
|
52
|
+
.num, .dur, .meth, .stat .sval, .tlbl { font-variant-numeric: tabular-nums; }
|
|
53
|
+
/* ── utility colours ──────────────────────────────────────────────────────── */
|
|
54
|
+
.green { color: var(--green); }
|
|
55
|
+
.yellow { color: var(--yellow); }
|
|
56
|
+
.red { color: var(--red); }
|
|
57
|
+
.cyan { color: var(--cyan); }
|
|
58
|
+
.dim { color: var(--muted); }
|
|
59
|
+
/* ── bar ──────────────────────────────────────────────────────────────────── */
|
|
60
|
+
#bar {
|
|
61
|
+
height: 32px; background: var(--bg); border-top: 1px solid var(--bdr);
|
|
62
|
+
display: flex; align-items: center; gap: 5px; padding: 0 8px;
|
|
63
|
+
cursor: pointer; user-select: none; overflow: hidden; flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
.logo { color: var(--purple); font-weight: 700; font-size: 13px; flex-shrink: 0; }
|
|
66
|
+
.dot { font-size: 8px; }
|
|
67
|
+
.dot.ok { color: var(--green); }
|
|
68
|
+
.dot.err { color: var(--red); animation: blink 1s step-start infinite; }
|
|
69
|
+
@keyframes blink { 50% { opacity: 0.25; } }
|
|
70
|
+
.bdiv { color: var(--bdr); flex-shrink: 0; }
|
|
71
|
+
.sp { flex: 1; }
|
|
72
|
+
.meth { font-weight: 700; font-size: 11px; flex-shrink: 0; }
|
|
73
|
+
.meth.get { color: var(--green); }
|
|
74
|
+
.meth.post { color: var(--cyan); }
|
|
75
|
+
.meth.put, .meth.patch { color: var(--yellow); }
|
|
76
|
+
.meth.delete { color: var(--red); }
|
|
77
|
+
/* Not an HTTP method: a Flow action, which arrives over the socket against a
|
|
78
|
+
synthetic GET of its own page. Its own colour so the list does not read as two
|
|
79
|
+
loads of that page. */
|
|
80
|
+
.meth.flow { color: var(--purple); }
|
|
81
|
+
.bpath { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
82
|
+
.sc { font-weight: 700; font-size: 11px; flex-shrink: 0; }
|
|
83
|
+
.sc.ok { color: var(--green); }
|
|
84
|
+
.sc.redir { color: var(--cyan); }
|
|
85
|
+
.sc.cli { color: var(--yellow); }
|
|
86
|
+
.sc.srv { color: var(--red); }
|
|
87
|
+
.chip {
|
|
88
|
+
font-size: 10px; padding: 1px 5px; border-radius: 999px;
|
|
89
|
+
background: var(--card); border: 1px solid var(--bdr); white-space: nowrap; flex-shrink: 0;
|
|
90
|
+
}
|
|
91
|
+
.chip.warn { border-color: var(--yellow); color: var(--yellow); }
|
|
92
|
+
.chip.ok { border-color: var(--green); color: var(--green); }
|
|
93
|
+
.ibtn {
|
|
94
|
+
height: 22px; padding: 0 6px;
|
|
95
|
+
background: var(--card); border: 1px solid var(--bdr); color: var(--text);
|
|
96
|
+
cursor: pointer; border-radius: 4px; font-size: 11px; font-family: inherit;
|
|
97
|
+
display: flex; align-items: center; gap: 3px; flex-shrink: 0;
|
|
98
|
+
}
|
|
99
|
+
.ibtn:hover { background: var(--surf); border-color: var(--purple); }
|
|
100
|
+
.ibtn.live-on { border-color: var(--green); color: var(--green); }
|
|
101
|
+
/* New traces arrived while pinned — an offer, never a jump. */
|
|
102
|
+
.ibtn.pending { border-color: var(--purple); color: var(--purple); animation: pulse 2s ease-in-out infinite; }
|
|
103
|
+
@keyframes pulse { 50% { opacity: 0.55; } }
|
|
104
|
+
/* ── panel ────────────────────────────────────────────────────────────────── */
|
|
105
|
+
#panel {
|
|
106
|
+
height: 380px; background: var(--bg); border-top: 1px solid var(--bdr);
|
|
107
|
+
display: flex; flex-direction: column; position: relative;
|
|
108
|
+
}
|
|
109
|
+
/* Drag strip. Sits above the tab row and overhangs upward so it is grabbable
|
|
110
|
+
without stealing clicks from the host page's last few pixels. */
|
|
111
|
+
#grip {
|
|
112
|
+
position: absolute; top: -3px; left: 0; right: 0; height: 7px;
|
|
113
|
+
cursor: ns-resize; z-index: 2;
|
|
114
|
+
}
|
|
115
|
+
#grip:hover, #grip.dragging { background: var(--purple); opacity: .5; }
|
|
116
|
+
#tabs {
|
|
117
|
+
display: flex; gap: 2px; padding: 4px 8px 0;
|
|
118
|
+
background: var(--surf); border-bottom: 1px solid var(--bdr);
|
|
119
|
+
flex-shrink: 0; overflow-x: auto;
|
|
120
|
+
}
|
|
121
|
+
.tab {
|
|
122
|
+
height: 28px; padding: 0 10px; background: transparent; border: none;
|
|
123
|
+
border-bottom: 2px solid transparent; color: var(--muted); cursor: pointer;
|
|
124
|
+
font: inherit; font-size: 11px; white-space: nowrap;
|
|
125
|
+
display: flex; align-items: center; gap: 4px;
|
|
126
|
+
}
|
|
127
|
+
.tab:hover { color: var(--text); }
|
|
128
|
+
.tab.active { color: var(--text); border-bottom-color: var(--purple); }
|
|
129
|
+
.tbdg {
|
|
130
|
+
font-size: 10px; padding: 0 4px; border-radius: 999px;
|
|
131
|
+
background: var(--card); min-width: 16px; text-align: center;
|
|
132
|
+
}
|
|
133
|
+
.tbdg.warn { background: transparent; color: var(--yellow); }
|
|
134
|
+
.ldot { font-size: 8px; color: var(--green); animation: blink 1.5s step-start infinite; }
|
|
135
|
+
#content { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; }
|
|
136
|
+
/* ── content shared ───────────────────────────────────────────────────────── */
|
|
137
|
+
.empty { color: var(--muted); text-align: center; padding: 40px 20px; }
|
|
138
|
+
.sec { padding: 10px 12px; border-bottom: 1px solid var(--bdr); }
|
|
139
|
+
.sec:last-child { border-bottom: none; }
|
|
140
|
+
.stitle {
|
|
141
|
+
font-size: 10px; text-transform: uppercase; letter-spacing: .6px;
|
|
142
|
+
color: var(--muted); margin-bottom: 8px;
|
|
143
|
+
display: flex; align-items: center; gap: 6px;
|
|
144
|
+
}
|
|
145
|
+
.stats { display: flex; flex-wrap: wrap; gap: 1px; background: var(--bdr); border-bottom: 1px solid var(--bdr); }
|
|
146
|
+
.stat { flex: 1; min-width: 90px; padding: 8px 12px; background: var(--bg); }
|
|
147
|
+
.slbl { font-size: 10px; color: var(--muted); margin-bottom: 2px; }
|
|
148
|
+
.sval { font-weight: 700; font-size: 13px; }
|
|
149
|
+
.rcard { padding: 8px 12px; border-bottom: 1px solid var(--bdr); background: var(--surf); font-size: 12px; }
|
|
150
|
+
/* ── copy buttons ─────────────────────────────────────────────────────────── */
|
|
151
|
+
.cpy {
|
|
152
|
+
background: transparent; border: 1px solid transparent; color: var(--muted);
|
|
153
|
+
font: inherit; font-size: 10px; line-height: 1; padding: 1px 4px;
|
|
154
|
+
border-radius: 3px; cursor: pointer; flex-shrink: 0; opacity: 0;
|
|
155
|
+
transition: opacity .1s;
|
|
156
|
+
}
|
|
157
|
+
.cpy:hover { color: var(--purple); border-color: var(--bdr); }
|
|
158
|
+
.cpy.done { color: var(--green); opacity: 1; }
|
|
159
|
+
.qrow:hover .cpy, .lrow:hover .cpy, .sec:hover .cpy, .tleaf:hover .cpy,
|
|
160
|
+
.tbranch > summary:hover .cpy, .crow:hover .cpy, .cpy:focus { opacity: 1; }
|
|
161
|
+
/* ── queries ──────────────────────────────────────────────────────────────── */
|
|
162
|
+
.qrow { padding: 8px 12px; border-bottom: 1px solid var(--bdr); }
|
|
163
|
+
.qrow:last-child { border-bottom: none; }
|
|
164
|
+
.qmeta { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
165
|
+
.qdur { font-weight: 700; font-size: 11px; min-width: 44px; }
|
|
166
|
+
.qbar { flex: 1; height: 4px; background: var(--card); border-radius: 2px; max-width: 120px; }
|
|
167
|
+
.qfill { height: 100%; background: var(--purple); border-radius: 2px; }
|
|
168
|
+
.qsql { font-size: 11px; white-space: pre-wrap; word-break: break-all; }
|
|
169
|
+
.qbind { font-size: 10px; color: var(--muted); margin-top: 3px; }
|
|
170
|
+
.bind { color: var(--orange); }
|
|
171
|
+
.wrow { padding: 8px 12px; border-bottom: 1px solid var(--bdr); border-left: 3px solid var(--yellow); }
|
|
172
|
+
.whead { color: var(--yellow); margin-bottom: 4px; font-size: 12px; }
|
|
173
|
+
.wfix { font-size: 10px; color: var(--muted); margin-top: 5px; }
|
|
174
|
+
.wfix code {
|
|
175
|
+
font: inherit; color: var(--cyan); background: var(--card);
|
|
176
|
+
padding: 1px 4px; border-radius: 3px;
|
|
177
|
+
}
|
|
178
|
+
/* ── exception banner ─────────────────────────────────────────────────────── */
|
|
179
|
+
.exc {
|
|
180
|
+
padding: 9px 12px; background: var(--card); border-left: 3px solid var(--red);
|
|
181
|
+
border-bottom: 1px solid var(--bdr);
|
|
182
|
+
}
|
|
183
|
+
.exhead { color: var(--red); font-weight: 700; font-size: 11px; margin-bottom: 3px; display: flex; align-items: center; gap: 6px; }
|
|
184
|
+
.exmsg { font-size: 11px; white-space: pre-wrap; word-break: break-word; }
|
|
185
|
+
/* ── logs ─────────────────────────────────────────────────────────────────── */
|
|
186
|
+
.lrow { display: flex; gap: 6px; padding: 4px 12px; border-bottom: 1px solid var(--bdr); }
|
|
187
|
+
.ltime { color: var(--muted); min-width: 54px; font-size: 10px; }
|
|
188
|
+
.llvl { min-width: 38px; font-weight: 700; font-size: 11px; }
|
|
189
|
+
.llvl.log, .llvl.debug, .llvl.info { color: var(--cyan); }
|
|
190
|
+
.llvl.warn { color: var(--yellow); }
|
|
191
|
+
.llvl.error { color: var(--red); }
|
|
192
|
+
.lmsg { font-size: 11px; white-space: pre-wrap; word-break: break-all; flex: 1; }
|
|
193
|
+
/* ── request kv table ─────────────────────────────────────────────────────── */
|
|
194
|
+
.kv { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
195
|
+
.kv td { padding: 4px 12px; border-bottom: 1px solid var(--bdr); vertical-align: top; }
|
|
196
|
+
.kv td:first-child { color: var(--muted); min-width: 160px; font-size: 10px; }
|
|
197
|
+
/* ── cards (mail / jobs) ──────────────────────────────────────────────────── */
|
|
198
|
+
.card { padding: 9px 12px; border-bottom: 1px solid var(--bdr); }
|
|
199
|
+
/* ── mail preview ─────────────────────────────────────────────────────────── */
|
|
200
|
+
.mprev { margin-top: 7px; }
|
|
201
|
+
.mprev > summary {
|
|
202
|
+
cursor: pointer; font-size: 10px; color: var(--muted);
|
|
203
|
+
list-style: none; user-select: none; width: fit-content;
|
|
204
|
+
}
|
|
205
|
+
.mprev > summary::-webkit-details-marker { display: none; }
|
|
206
|
+
.mprev > summary::before { content: '▸ '; }
|
|
207
|
+
.mprev[open] > summary::before { content: '▾ '; }
|
|
208
|
+
.mprev > summary:hover { color: var(--purple); }
|
|
209
|
+
.mframe {
|
|
210
|
+
width: 100%; height: 320px; margin-top: 6px; border: 1px solid var(--bdr);
|
|
211
|
+
border-radius: 4px; background: #fff;
|
|
212
|
+
}
|
|
213
|
+
/* ── all-requests list ────────────────────────────────────────────────────── */
|
|
214
|
+
/* Fixed height: the virtualiser positions rows arithmetically above ~200 of
|
|
215
|
+
them, and a row that can grow would put every offset out. */
|
|
216
|
+
.hrow {
|
|
217
|
+
display: flex; align-items: center; gap: 6px;
|
|
218
|
+
padding: 0 12px; height: 26px; border-bottom: 1px solid var(--bdr);
|
|
219
|
+
cursor: pointer; font-size: 11px; overflow: hidden;
|
|
220
|
+
}
|
|
221
|
+
.hrow:hover { background: var(--surf); }
|
|
222
|
+
.hrow.cur { background: var(--card); }
|
|
223
|
+
.hrow.on { box-shadow: inset 2px 0 0 var(--purple); }
|
|
224
|
+
.hrow.err { border-left: 3px solid var(--red); padding-left: 9px; }
|
|
225
|
+
.hrow.child { padding-left: 26px; background: var(--childbg); }
|
|
226
|
+
.hpath { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
227
|
+
.hexc { color: var(--red); font-size: 10px; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
228
|
+
.gtog {
|
|
229
|
+
font-size: 10px; padding: 0 5px; border-radius: 999px; cursor: pointer;
|
|
230
|
+
background: var(--card); border: 1px solid var(--purple); color: var(--purple);
|
|
231
|
+
font-family: inherit; flex-shrink: 0;
|
|
232
|
+
}
|
|
233
|
+
.gtog:hover { background: var(--surf); }
|
|
234
|
+
/* Windowed rendering: two spacers stand in for the rows that are not drawn. */
|
|
235
|
+
.vpad { flex-shrink: 0; }
|
|
236
|
+
/* ── filter bar ───────────────────────────────────────────────────────────── */
|
|
237
|
+
.fbar {
|
|
238
|
+
display: flex; gap: 6px; align-items: center; padding: 6px 12px;
|
|
239
|
+
border-bottom: 1px solid var(--bdr); background: var(--surf);
|
|
240
|
+
position: sticky; top: 0; z-index: 1;
|
|
241
|
+
}
|
|
242
|
+
.finput {
|
|
243
|
+
flex: 1; min-width: 90px; height: 24px; padding: 0 8px; font: inherit; font-size: 11px;
|
|
244
|
+
background: var(--bg); border: 1px solid var(--bdr); border-radius: 4px; color: var(--text);
|
|
245
|
+
}
|
|
246
|
+
.finput:focus { outline: none; border-color: var(--purple); }
|
|
247
|
+
.finput::placeholder { color: var(--muted); }
|
|
248
|
+
.facets {
|
|
249
|
+
display: flex; gap: 4px; align-items: center; flex-wrap: wrap;
|
|
250
|
+
padding: 5px 12px; border-bottom: 1px solid var(--bdr); background: var(--surf);
|
|
251
|
+
}
|
|
252
|
+
.fchip {
|
|
253
|
+
font: inherit; font-size: 10px; padding: 1px 6px; border-radius: 999px; cursor: pointer;
|
|
254
|
+
background: transparent; border: 1px solid var(--bdr); color: var(--muted);
|
|
255
|
+
}
|
|
256
|
+
.fchip:hover { color: var(--text); border-color: var(--muted); }
|
|
257
|
+
.fchip.on { background: var(--card); color: var(--purple); border-color: var(--purple); }
|
|
258
|
+
.fchip.on.warn { color: var(--yellow); border-color: var(--yellow); }
|
|
259
|
+
.fsep { width: 1px; height: 12px; background: var(--bdr); margin: 0 3px; }
|
|
260
|
+
/* ── timeline waterfall ───────────────────────────────────────────────────── */
|
|
261
|
+
.trow { display: flex; align-items: center; gap: 8px; padding: 3px 12px; font-size: 11px; }
|
|
262
|
+
.trow:hover { background: var(--surf); }
|
|
263
|
+
.tlbl { min-width: 78px; font-size: 10px; text-align: right; flex-shrink: 0; }
|
|
264
|
+
.ttrack { flex: 1; height: 12px; position: relative; background: var(--card); border-radius: 2px; }
|
|
265
|
+
.tmark {
|
|
266
|
+
position: absolute; top: 0; height: 12px; min-width: 3px; border-radius: 2px;
|
|
267
|
+
background: var(--purple);
|
|
268
|
+
}
|
|
269
|
+
.tmark.query { background: var(--purple); }
|
|
270
|
+
.tmark.cache { background: var(--cyan); }
|
|
271
|
+
.tmark.mail { background: var(--green); }
|
|
272
|
+
.tmark.job { background: var(--orange); }
|
|
273
|
+
.tmark.log { background: var(--muted); }
|
|
274
|
+
.tmark.warn { background: var(--red); }
|
|
275
|
+
.tmark.chan { background: var(--yellow); }
|
|
276
|
+
.ttxt { flex: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
|
|
277
|
+
.tkey { display: flex; gap: 10px; flex-wrap: wrap; padding: 6px 12px; font-size: 10px; color: var(--muted); }
|
|
278
|
+
/* position:static undoes .tmark, which the legend swatch reuses for its colour.
|
|
279
|
+
.tmark is absolutely positioned for the waterfall, and in the legend — whose
|
|
280
|
+
rows are not positioned — that sent every swatch to the nearest positioned
|
|
281
|
+
ancestor, leaving seven squares stacked above their own labels. */
|
|
282
|
+
.tkey i { position: static; display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 3px; vertical-align: middle; font-style: normal; }
|
|
283
|
+
/* ── an open request ──────────────────────────────────────────────────────── */
|
|
284
|
+
.lvhead {
|
|
285
|
+
display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;
|
|
286
|
+
border-bottom: 1px solid var(--bdr); background: var(--surf);
|
|
287
|
+
position: sticky; top: 0; z-index: 1;
|
|
288
|
+
}
|
|
289
|
+
.hchev { width: 12px; flex-shrink: 0; color: var(--muted); text-align: center; }
|
|
290
|
+
.hdetail {
|
|
291
|
+
background: var(--childbg); border-bottom: 1px solid var(--bdr);
|
|
292
|
+
border-left: 2px solid var(--purple);
|
|
293
|
+
}
|
|
294
|
+
/* The request's own tab strip — alternatives, not a stack you scroll past. */
|
|
295
|
+
.dsecs {
|
|
296
|
+
display: flex; gap: 2px; align-items: center; flex-wrap: wrap;
|
|
297
|
+
padding: 4px 10px; border-bottom: 1px solid var(--bdr); background: var(--surf);
|
|
298
|
+
}
|
|
299
|
+
.dsect {
|
|
300
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
301
|
+
background: none; border: 0; border-radius: 3px; cursor: pointer;
|
|
302
|
+
padding: 3px 7px; font: inherit; font-size: 11px; color: var(--muted);
|
|
303
|
+
}
|
|
304
|
+
.dsect:hover { color: var(--text); background: var(--card); }
|
|
305
|
+
.dsect.on { color: var(--text); background: var(--card); font-weight: 700; }
|
|
306
|
+
.tbtn {
|
|
307
|
+
background: none; border: 1px solid var(--bdr); border-radius: 3px;
|
|
308
|
+
color: var(--muted); cursor: pointer; padding: 2px 8px; font: inherit; font-size: 11px;
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
}
|
|
311
|
+
.tbtn:hover { color: var(--text); border-color: var(--muted); }
|
|
312
|
+
.dsec-n {
|
|
313
|
+
background: var(--card); color: var(--text); border-radius: 6px;
|
|
314
|
+
padding: 0 5px; font-size: 10px; font-variant-numeric: tabular-nums;
|
|
315
|
+
}
|
|
316
|
+
.dsec-n.warn { background: var(--red); color: var(--bg); }
|
|
317
|
+
/* ── generic channel rows ─────────────────────────────────────────────────── */
|
|
318
|
+
.crow { padding: 7px 12px; border-bottom: 1px solid var(--bdr); }
|
|
319
|
+
.crow.warn { border-left: 3px solid var(--yellow); }
|
|
320
|
+
.chead { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
|
|
321
|
+
.cttl { font-size: 11px; word-break: break-word; }
|
|
322
|
+
.cmeta { font-size: 10px; color: var(--muted); display: flex; gap: 10px; flex-wrap: wrap; }
|
|
323
|
+
/* Badge accents. Assigned by hashing the badge's own text, so repeated values
|
|
324
|
+
stay the same colour and devtools needs no vocabulary to tell them apart. */
|
|
325
|
+
.chip.a0 { border-color: var(--purple); color: var(--purple); }
|
|
326
|
+
.chip.a1 { border-color: var(--cyan); color: var(--cyan); }
|
|
327
|
+
.chip.a2 { border-color: var(--green); color: var(--green); }
|
|
328
|
+
.chip.a3 { border-color: var(--orange); color: var(--orange); }
|
|
329
|
+
.chip.a4 { border-color: var(--yellow); color: var(--yellow); }
|
|
330
|
+
.chip.a5 { border-color: var(--red); color: var(--red); }
|
|
331
|
+
.chip.flag { font-size: 9px; padding: 0 4px; text-transform: uppercase; letter-spacing: .4px; }
|
|
332
|
+
/* ── channel: tree ────────────────────────────────────────────────────────── */
|
|
333
|
+
.tnode { border-bottom: 1px solid var(--bdr); }
|
|
334
|
+
.tbranch > summary {
|
|
335
|
+
padding: 5px 12px; cursor: pointer; list-style: none; user-select: none;
|
|
336
|
+
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
|
337
|
+
}
|
|
338
|
+
.tbranch > summary::-webkit-details-marker { display: none; }
|
|
339
|
+
.tbranch > summary::before { content: '▸'; color: var(--muted); font-size: 9px; }
|
|
340
|
+
.tbranch[open] > summary::before { content: '▾'; }
|
|
341
|
+
.tbranch > summary:hover { background: var(--surf); }
|
|
342
|
+
.tleaf { padding: 5px 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11px; }
|
|
343
|
+
.tleaf:hover { background: var(--surf); }
|
|
344
|
+
.tname { font-weight: 600; }
|
|
345
|
+
.tattr { font-size: 10px; color: var(--muted); }
|
|
346
|
+
.tkids { border-left: 1px solid var(--bdr); margin-left: 17px; }
|
|
347
|
+
/* ── channel: table ───────────────────────────────────────────────────────── */
|
|
348
|
+
.ctbl { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
349
|
+
.ctbl th {
|
|
350
|
+
text-align: left; padding: 5px 12px; font-size: 10px; font-weight: 400;
|
|
351
|
+
text-transform: uppercase; letter-spacing: .5px; color: var(--muted);
|
|
352
|
+
background: var(--surf); border-bottom: 1px solid var(--bdr);
|
|
353
|
+
position: sticky; top: 0;
|
|
354
|
+
}
|
|
355
|
+
.ctbl td { padding: 5px 12px; border-bottom: 1px solid var(--bdr); vertical-align: top; }
|
|
356
|
+
.ctbl tr.warn td:first-child { box-shadow: inset 3px 0 0 var(--yellow); }
|
|
357
|
+
/* ── channel: grouped ─────────────────────────────────────────────────────── */
|
|
358
|
+
.cgrp > summary {
|
|
359
|
+
padding: 6px 12px; cursor: pointer; list-style: none; user-select: none;
|
|
360
|
+
background: var(--surf); border-bottom: 1px solid var(--bdr);
|
|
361
|
+
display: flex; align-items: center; gap: 8px; font-size: 11px;
|
|
362
|
+
}
|
|
363
|
+
.cgrp > summary::-webkit-details-marker { display: none; }
|
|
364
|
+
.cgrp > summary::before { content: '▸'; color: var(--muted); font-size: 9px; }
|
|
365
|
+
.cgrp[open] > summary::before { content: '▾'; }
|
|
366
|
+
/* ── section switcher ─────────────────────────────────────────────────────── */
|
|
367
|
+
.sects { display: flex; gap: 1px; align-self: center; flex-shrink: 0; margin-bottom: 4px; }
|
|
368
|
+
.sect {
|
|
369
|
+
font: inherit; font-size: 10px; padding: 3px 9px; cursor: pointer;
|
|
370
|
+
background: var(--bg); border: 1px solid var(--bdr); color: var(--muted);
|
|
371
|
+
text-transform: uppercase; letter-spacing: .5px;
|
|
372
|
+
}
|
|
373
|
+
.sect:first-child { border-radius: 4px 0 0 4px; }
|
|
374
|
+
.sect:last-child { border-radius: 0 4px 4px 0; }
|
|
375
|
+
.sect:hover { color: var(--text); }
|
|
376
|
+
.sect.on { background: var(--card); color: var(--purple); border-color: var(--purple); }
|
|
377
|
+
.tabdiv { width: 1px; background: var(--bdr); align-self: stretch; margin: 4px 6px 0; flex-shrink: 0; }
|
|
378
|
+
a.link { color: var(--cyan); text-decoration: none; }
|
|
379
|
+
a.link:hover { text-decoration: underline; }
|
|
380
|
+
/* ── source locations ─────────────────────────────────────────────────────── */
|
|
381
|
+
.src {
|
|
382
|
+
font-size: 10px; color: var(--muted); flex-shrink: 0; white-space: nowrap;
|
|
383
|
+
max-width: 220px; overflow: hidden; text-overflow: ellipsis;
|
|
384
|
+
}
|
|
385
|
+
a.src.link { color: var(--cyan); text-decoration: none; }
|
|
386
|
+
a.src.link:hover { text-decoration: underline; }
|
|
387
|
+
/* ── exception frames ─────────────────────────────────────────────────────── */
|
|
388
|
+
.frame {
|
|
389
|
+
display: flex; align-items: baseline; gap: 8px; justify-content: space-between;
|
|
390
|
+
padding: 4px 12px; border-bottom: 1px solid var(--bdr); font-size: 11px;
|
|
391
|
+
}
|
|
392
|
+
.frame:hover { background: var(--surf); }
|
|
393
|
+
/* Framework frames are context, not the answer — dimmed so your own code stands
|
|
394
|
+
out of a forty-frame trace without being hidden from it. */
|
|
395
|
+
.frame.vendor { opacity: .5; }
|
|
396
|
+
.frame .src { max-width: 60%; }
|
|
397
|
+
.fnname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
398
|
+
/* ── chip rows (session keys) ─────────────────────────────────────────────── */
|
|
399
|
+
.chips { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
400
|
+
/* ── keyboard help ────────────────────────────────────────────────────────── */
|
|
401
|
+
.keys { padding: 6px 12px; font-size: 10px; color: var(--muted); display: flex; gap: 12px; flex-wrap: wrap; border-top: 1px solid var(--bdr); }
|
|
402
|
+
.keys kbd {
|
|
403
|
+
font: inherit; background: var(--card); border: 1px solid var(--bdr);
|
|
404
|
+
border-radius: 3px; padding: 0 3px; color: var(--text);
|
|
405
|
+
}
|
|
406
|
+
/* ── standalone dashboard ─────────────────────────────────────────────────── */
|
|
407
|
+
#wrap.standalone { display: flex; flex-direction: column; height: 100vh; background: var(--bg); }
|
|
408
|
+
#wrap.standalone #panel { flex: 1; height: auto !important; border-top: none; }
|
|
409
|
+
#wrap.standalone #grip { display: none; }
|
|
410
|
+
#wrap.standalone #bar { border-top: none; border-bottom: 1px solid var(--bdr); cursor: default; order: -1; }
|
|
411
|
+
</style>`;
|
|
412
|
+
|
|
413
|
+
/** What the user asked for, which is not the same as which palette is showing. */
|
|
414
|
+
export type ThemeChoice = "auto" | "dark" | "light";
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resolve the three-way choice to the one class the stylesheet understands.
|
|
418
|
+
*
|
|
419
|
+
* `auto` asks the browser, and the panel re-asks whenever the system flips, so a
|
|
420
|
+
* machine that switches at sunset does not leave a dark panel on a light page.
|
|
421
|
+
*/
|
|
422
|
+
export function isLightTheme(choice: ThemeChoice): boolean {
|
|
423
|
+
if (choice === "light") return true;
|
|
424
|
+
if (choice === "dark") return false;
|
|
425
|
+
try {
|
|
426
|
+
return window.matchMedia("(prefers-color-scheme: light)").matches;
|
|
427
|
+
} catch {
|
|
428
|
+
// No matchMedia (a very old browser, a test harness) — dark is the default
|
|
429
|
+
// the panel has always had.
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The next choice in the toggle's cycle, and the icon that stands for it. */
|
|
435
|
+
export const THEME_CYCLE: Record<ThemeChoice, ThemeChoice> = {
|
|
436
|
+
auto: "dark",
|
|
437
|
+
dark: "light",
|
|
438
|
+
light: "auto",
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export const THEME_ICON: Record<ThemeChoice, string> = {
|
|
442
|
+
auto: "◐",
|
|
443
|
+
dark: "●",
|
|
444
|
+
light: "○",
|
|
445
|
+
};
|
package/src/client-auto.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { deepMerge } from "@zerotal/core";
|
|
2
2
|
import type { RedactionOptions } from "./redaction.ts";
|
|
3
|
+
import type { EditorName } from "./editor.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decides whether a request may reach the inspector outside a development
|
|
7
|
+
* process. Returning anything but `true` is a refusal.
|
|
8
|
+
*/
|
|
9
|
+
export type DevtoolsGate = (request: Request) => boolean | Promise<boolean>;
|
|
3
10
|
|
|
4
11
|
export interface DevtoolsConfigShape {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the inspector runs at all.
|
|
14
|
+
*
|
|
15
|
+
* `null` (the default) follows the same dev-surface gate as the stack-trace
|
|
16
|
+
* error page: on under `zt dev`, off in a deployed process. `true` or `false`
|
|
17
|
+
* decides explicitly — and `true` outside development requires a {@link gate},
|
|
18
|
+
* because the absence of one is a refusal rather than a default allow.
|
|
19
|
+
*/
|
|
20
|
+
enabled: boolean | null;
|
|
21
|
+
/**
|
|
22
|
+
* Who may read the inspector outside a development process.
|
|
23
|
+
*
|
|
24
|
+
* Never consulted on a development machine: a gate that can lock you out of
|
|
25
|
+
* your own laptop is a gate that gets switched off, and then nothing is gated.
|
|
26
|
+
* One function answers for every endpoint — the stream, the trace JSON, the
|
|
27
|
+
* dashboard, and the panel bundle are the same secret.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* gate: (request) => request.headers.get("X-Debug-Key") === Bun.env["DEBUG_KEY"],
|
|
31
|
+
*/
|
|
32
|
+
gate: DevtoolsGate | null;
|
|
5
33
|
/**
|
|
6
34
|
* How many request traces to keep in memory and reload on start.
|
|
7
35
|
* Default: `100`.
|
|
@@ -19,21 +47,67 @@ export interface DevtoolsConfigShape {
|
|
|
19
47
|
*/
|
|
20
48
|
pruneHours: number;
|
|
21
49
|
/**
|
|
22
|
-
* Whether
|
|
50
|
+
* Whether sensitive values are masked before a trace leaves the process.
|
|
23
51
|
*
|
|
24
52
|
* On by default: a trace is streamed to the browser *and* written to disk for
|
|
25
|
-
* a day, and
|
|
53
|
+
* a day, and what it carries is the request's real values — the password on a
|
|
26
54
|
* registration, a reset token, every customer email a listing selects by.
|
|
27
55
|
* Turn it off only when you are debugging the values themselves.
|
|
28
56
|
*/
|
|
29
57
|
redact: RedactionOptions;
|
|
58
|
+
/**
|
|
59
|
+
* Which editor `file:line` links open.
|
|
60
|
+
*
|
|
61
|
+
* Every location the panel shows becomes a link: a query's call site, a log
|
|
62
|
+
* line's, a stack frame, a prop's render source. Going from "this query is
|
|
63
|
+
* slow" to the line that ran it is the most frequent move in a debugging
|
|
64
|
+
* session, and it was two manual searches.
|
|
65
|
+
*
|
|
66
|
+
* Default: `vscode`. Set to `null` to render locations as plain text.
|
|
67
|
+
*/
|
|
68
|
+
editor: EditorName | null;
|
|
69
|
+
/**
|
|
70
|
+
* Rewrite a captured path before it becomes a link — for editing on a machine
|
|
71
|
+
* that is not the one running the code.
|
|
72
|
+
*
|
|
73
|
+
* Keys are path prefixes as the *server* sees them; values are what your editor
|
|
74
|
+
* should open instead. A container reporting `/app/src/Foo.ts` maps home with
|
|
75
|
+
* `{ "/app": "/Users/you/project" }`.
|
|
76
|
+
*/
|
|
77
|
+
editorPathMap: Record<string, string>;
|
|
78
|
+
/**
|
|
79
|
+
* Capture the application call site for each query and log line.
|
|
80
|
+
*
|
|
81
|
+
* "Which of my 40 queries is slow" was answerable; "where do I go to fix it"
|
|
82
|
+
* was not. One stack walk per recorded event, filtered to application frames —
|
|
83
|
+
* measured at roughly two microseconds regardless of stack depth, so about
|
|
84
|
+
* 0.08ms on a request running forty queries.
|
|
85
|
+
*
|
|
86
|
+
* Default: on. It only ever runs when the inspector itself is running.
|
|
87
|
+
*/
|
|
88
|
+
captureSource: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Extra request headers to record, on top of the built-in safe list.
|
|
91
|
+
*
|
|
92
|
+
* The allowlist exists because a trace is persisted: `cookie` and
|
|
93
|
+
* `authorization` are the request's credentials. This opens up the ones you
|
|
94
|
+
* are actually debugging — matched case-insensitively, and `"*"` records every
|
|
95
|
+
* header except those `redact` masks.
|
|
96
|
+
*/
|
|
97
|
+
headers: string[];
|
|
30
98
|
}
|
|
31
99
|
|
|
32
100
|
const defaults: DevtoolsConfigShape = {
|
|
101
|
+
enabled: null,
|
|
102
|
+
gate: null,
|
|
33
103
|
capacity: 100,
|
|
34
104
|
dbPath: Bun.env["ZT_DEVTOOLS_DB"] ?? ".zerotal/devtools.sqlite",
|
|
35
105
|
pruneHours: Number(Bun.env["ZT_DEVTOOLS_PRUNE_HOURS"] ?? 24),
|
|
36
106
|
redact: { enabled: true, allow: [], deny: [] },
|
|
107
|
+
editor: "vscode",
|
|
108
|
+
editorPathMap: {},
|
|
109
|
+
captureSource: true,
|
|
110
|
+
headers: [],
|
|
37
111
|
};
|
|
38
112
|
|
|
39
113
|
/**
|
|
@@ -45,6 +119,7 @@ const defaults: DevtoolsConfigShape = {
|
|
|
45
119
|
*
|
|
46
120
|
* export default DevtoolsConfig({
|
|
47
121
|
* capacity: 250,
|
|
122
|
+
* editor: 'cursor',
|
|
48
123
|
* redact: { allow: ['email', 'slug'] },
|
|
49
124
|
* });
|
|
50
125
|
*/
|
package/src/dashboard-auto.ts
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* shadowed and never gained the plugin tabs the panel had. Sharing the renderer
|
|
9
9
|
* means a tab added anywhere shows up in both.
|
|
10
10
|
*/
|
|
11
|
-
import { DevTools } from "./client.ts";
|
|
11
|
+
import { DevTools } from "./client/index.ts";
|
|
12
12
|
|
|
13
13
|
DevTools.start({ mode: "standalone" });
|