@beignet/devtools 0.0.5 → 0.0.7

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/src/ui.ts CHANGED
@@ -3,14 +3,17 @@ import {
3
3
  devtoolsNotFoundResponse,
4
4
  isDevtoolsRouteEnabled,
5
5
  } from "./access.js";
6
- import { devtoolsPanelModelBrowserSource } from "./ui-model.js";
6
+ import { DEVTOOLS_DASHBOARD_HTML } from "./ui-html.generated.js";
7
7
 
8
8
  /**
9
9
  * Devtools UI handler
10
10
  *
11
- * Serves a self-contained HTML dashboard for viewing devtools events.
11
+ * Serves the self-contained dashboard built from `ui/` at package build time.
12
+ * The server injects the mounted base path into the embedded HTML.
12
13
  */
13
14
 
15
+ const BASE_PATH_PLACEHOLDER = "__BEIGNET_DEVTOOLS_BASE_PATH__";
16
+
14
17
  /**
15
18
  * Handle requests for the devtools dashboard UI.
16
19
  *
@@ -25,7 +28,7 @@ export function handleDevtoolsUIRequest(
25
28
  return devtoolsNotFoundResponse();
26
29
  }
27
30
 
28
- const html = buildDashboardHTML(basePath);
31
+ const html = renderDashboardHTML(basePath);
29
32
 
30
33
  return new Response(html, {
31
34
  status: 200,
@@ -33,931 +36,25 @@ export function handleDevtoolsUIRequest(
33
36
  });
34
37
  }
35
38
 
36
- function buildDashboardHTML(basePath: string): string {
39
+ function renderDashboardHTML(basePath: string): string {
37
40
  const sanitizedBasePath = basePath.replace(/\/+$/, "");
41
+ const escapedBasePath = escapeForJsonScript(sanitizedBasePath);
38
42
 
39
- return `<!DOCTYPE html>
40
- <html lang="en">
41
- <head>
42
- <meta charset="utf-8" />
43
- <meta name="viewport" content="width=device-width, initial-scale=1" />
44
- <title>Beignet Devtools</title>
45
- <style>
46
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
47
- :root{
48
- --bg:#ffffff;--surface:#f8fafc;--surface-2:#f1f5f9;--surface-3:#e2e8f0;--border:rgba(15,23,42,.1);
49
- --text:#0f172a;--text-muted:#475569;--text-dim:#94a3b8;
50
- --accent:#4f46e5;--accent-strong:#4338ca;
51
- --red:#e11d48;--orange:#d97706;--green:#059669;--blue:#2563eb;--purple:#7c3aed;--gray:#64748b;
52
- --shadow:0 20px 50px rgba(15,23,42,.06);
53
- --font-mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
54
- --font-sans:"Inter",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
55
- }
56
- body{font-family:var(--font-sans);background:var(--bg);color:var(--text);line-height:1.5;min-height:100vh;font-size:14px}
57
- .shell{max-width:1160px;margin:0 auto;padding:28px 28px 48px}
58
- header{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;padding:0 0 20px;border-bottom:1px solid var(--border);margin-bottom:18px}
59
- .brand{display:flex;align-items:center;gap:10px;color:var(--text);font-size:12px;font-weight:650;letter-spacing:.01em}
60
- .mark{width:22px;height:22px;border:1px solid var(--border);border-radius:6px;background:linear-gradient(180deg,#fff,var(--surface));display:grid;place-items:center;color:var(--accent-strong);font-family:var(--font-mono);font-size:10px;box-shadow:0 1px 2px rgba(15,23,42,.04)}
61
- h1{font-size:26px;line-height:1.15;font-weight:720;letter-spacing:-.01em;margin-top:18px}
62
- h1 span{color:var(--accent-strong);font-weight:720}
63
- .subtitle{color:var(--text-muted);font-size:13px;margin-top:8px;max-width:520px}
64
- .controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
65
- button{font-family:var(--font-sans);font-size:12px;font-weight:560;padding:7px 10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text-muted);cursor:pointer;transition:border-color .15s,background .15s,color .15s,box-shadow .15s}
66
- button:hover{border-color:rgba(79,70,229,.28);background:var(--surface);color:var(--text)}
67
- button.active{border-color:rgba(79,70,229,.3);color:var(--accent-strong);background:#eef2ff;box-shadow:inset 0 0 0 1px rgba(79,70,229,.04)}
68
- button.danger{color:var(--red)}
69
- button.danger:hover{border-color:rgba(225,29,72,.24);background:#fff1f2}
70
- .status{font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:7px;padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface)}
71
- .dot{width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block}
72
- .dot.paused{background:var(--text-dim)}
73
- .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}
74
- .stat{border:1px solid var(--border);background:var(--surface);border-radius:8px;padding:14px 14px 12px}
75
- .stat-label{color:var(--text-muted);font-size:11px;text-transform:uppercase;letter-spacing:.12em}
76
- .stat-value{font-family:var(--font-mono);font-size:22px;color:var(--text);margin-top:5px;letter-spacing:0}
77
- .panel{border:1px solid var(--border);border-radius:8px;background:var(--bg);box-shadow:var(--shadow);overflow:hidden}
78
- .panel-top{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px;border-bottom:1px solid var(--border);background:var(--surface)}
79
- .tabs{display:flex;gap:6px;flex-wrap:wrap}
80
- .tab-count{font-family:var(--font-mono);font-size:10px;color:var(--text-dim);margin-left:4px}
81
- .toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
82
- .toolbar input,.toolbar select{font-family:var(--font-mono);font-size:12px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);outline:none;height:34px}
83
- .toolbar input{width:300px}
84
- .toolbar select{min-width:118px}
85
- .toolbar input:focus,.toolbar select:focus{border-color:rgba(79,70,229,.45);box-shadow:0 0 0 3px rgba(79,70,229,.08)}
86
- .toolbar input::placeholder{color:var(--text-dim)}
87
- .event-list{display:flex;flex-direction:column}
88
- .event{background:var(--bg);padding:12px 14px;cursor:pointer;transition:background .1s,border-color .1s;border-bottom:1px solid var(--border)}
89
- .event:last-child{border-bottom:0}
90
- .event:hover{background:var(--surface)}
91
- .event.open{background:#f8fafc}
92
- .event-header{display:flex;align-items:center;gap:8px;font-size:13px}
93
- .event-time{color:var(--text-dim);font-family:var(--font-mono);font-size:11px;min-width:82px}
94
- .event-summary{color:var(--text-muted);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
95
- .event-detail{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);font-family:var(--font-mono);font-size:11px;color:var(--text-muted);line-height:1.7;white-space:pre-wrap;word-break:break-word}
96
- .event.open .event-detail{display:block}
97
- .request-detail{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr);gap:12px}
98
- .detail-section{background:var(--bg);border:1px solid var(--border);border-radius:7px;padding:10px}
99
- .detail-title{color:var(--text);font-family:var(--font-sans);font-size:12px;font-weight:650;margin-bottom:6px}
100
- .correlated{display:flex;flex-direction:column;gap:6px}
101
- .correlated-row{display:flex;gap:7px;align-items:flex-start;color:var(--text-muted)}
102
- .request-map{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}
103
- .request-fact{border:1px solid var(--border);border-radius:7px;background:var(--surface);padding:9px 10px;min-width:0}
104
- .request-fact-label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.1em}
105
- .request-fact-value{font-family:var(--font-mono);font-size:12px;color:var(--text);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
106
- .detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:10px}
107
- .lifecycle{display:flex;flex-direction:column;gap:7px}
108
- .phase-row{display:grid;grid-template-columns:108px minmax(130px,.8fr) minmax(180px,1.2fr) 92px;gap:8px;align-items:start;border:1px solid var(--border);border-radius:7px;background:var(--bg);padding:8px}
109
- .phase-name{font-family:var(--font-mono);font-size:11px;color:var(--text)}
110
- .phase-summary{color:var(--text-muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
111
- .phase-meta{font-family:var(--font-mono);font-size:10px;color:var(--text-dim)}
112
- .owner-pill{display:inline-block;border:1px solid var(--border);border-radius:999px;background:var(--surface-2);color:var(--text-muted);font-family:var(--font-mono);font-size:10px;padding:2px 6px;line-height:1.4}
113
- .owner-framework,.owner-provider,.owner-devtools{color:var(--red);background:#fff1f2;border-color:#ffe4e6}
114
- .owner-route{color:var(--green);background:#ecfdf5;border-color:#d1fae5}
115
- .owner-transport,.owner-client{color:var(--blue);background:#eff6ff;border-color:#dbeafe}
116
- .owner-job,.owner-schedule,.owner-outbox{color:var(--orange);background:#fff7ed;border-color:#fed7aa}
117
- .redaction-note{border:1px solid var(--border);border-radius:7px;background:var(--surface);padding:8px 10px;color:var(--text-muted);font-size:12px;margin-top:8px}
118
- .focus-panel{display:flex;flex-direction:column}
119
- .focus-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px;border-bottom:1px solid var(--border);background:var(--bg)}
120
- .focus-title{font-size:15px;line-height:1.2;font-weight:720;color:var(--text)}
121
- .focus-subtitle{margin-top:3px;color:var(--text-muted);font-size:12px}
122
- .focus-metrics{display:grid;grid-template-columns:repeat(4,minmax(92px,1fr));gap:8px;min-width:440px}
123
- .focus-metric{border:1px solid var(--border);border-radius:7px;background:var(--surface);padding:8px 9px}
124
- .focus-metric-label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.1em}
125
- .focus-metric-value{font-family:var(--font-mono);font-size:15px;color:var(--text);margin-top:3px}
126
- .event-table{display:flex;flex-direction:column}
127
- .table-row{display:grid;grid-template-columns:86px minmax(140px,.8fr) minmax(210px,1.2fr) minmax(130px,.8fr) 78px 104px;gap:10px;align-items:start;padding:11px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s}
128
- .table-row:last-child{border-bottom:0}
129
- .table-row:hover{background:var(--surface)}
130
- .table-row.open{background:#f8fafc}
131
- .table-cell{min-width:0;color:var(--text-muted);font-size:12px}
132
- .table-cell strong{display:block;color:var(--text);font-size:13px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
133
- .table-cell code{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:1px 4px}
134
- .table-summary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
135
- .table-correlation{display:flex;flex-wrap:wrap;gap:4px}
136
- .table-empty{font-family:var(--font-mono);font-size:10px;color:var(--text-dim)}
137
- .table-detail{display:none;grid-column:1/-1;margin-top:2px;padding-top:10px;border-top:1px solid var(--border);font-family:var(--font-mono);font-size:11px;color:var(--text-muted);line-height:1.7;white-space:pre-wrap;word-break:break-word}
138
- .table-row.open .table-detail{display:block}
139
- .table-detail-note{font-family:var(--font-sans);font-size:12px;color:var(--text-muted);margin-bottom:8px;white-space:normal}
140
- .result-chip{display:inline-block;border:1px solid var(--border);border-radius:5px;background:var(--surface);font-family:var(--font-mono);font-size:10px;color:var(--text-muted);padding:2px 5px}
141
- .result-failed,.result-blocked,.result-deadLettered{color:var(--red);background:#fff1f2;border-color:#ffe4e6}
142
- .result-retryScheduled{color:var(--orange);background:#fff7ed;border-color:#fed7aa}
143
- .result-delivered,.result-completed,.result-allowed,.result-hit,.result-signed_in{color:var(--green);background:#ecfdf5;border-color:#d1fae5}
144
- .error-board{display:flex;flex-direction:column}
145
- .error-owner-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;padding:14px;border-bottom:1px solid var(--border);background:var(--bg)}
146
- .error-owner-card{border:1px solid var(--border);border-radius:7px;background:var(--surface);padding:9px 10px;min-width:0}
147
- .error-owner-card strong{display:block;font-family:var(--font-mono);font-size:15px;color:var(--text)}
148
- .error-owner-card span{display:block;color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.1em;margin-top:2px}
149
- .error-group{border-bottom:1px solid var(--border)}
150
- .error-group:last-child{border-bottom:0}
151
- .error-group-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;background:var(--surface)}
152
- .error-group-title{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:700;color:var(--text)}
153
- .error-group-count{font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}
154
- .error-row{display:grid;grid-template-columns:86px minmax(170px,1fr) 90px minmax(150px,.9fr) minmax(140px,.8fr);gap:10px;align-items:start;padding:11px 14px;border-top:1px solid var(--border);cursor:pointer;transition:background .1s}
155
- .error-row:hover{background:var(--surface)}
156
- .error-row.open{background:#f8fafc}
157
- .error-message{min-width:0;color:var(--text);font-size:13px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
158
- .error-context{font-family:var(--font-mono);font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
159
- .error-related{display:flex;flex-direction:column;gap:5px;margin-top:8px}
160
- .empty{text-align:center;padding:58px 16px;color:var(--text-dim);font-size:13px;background:var(--bg)}
161
- .badge{display:inline-block;font-size:10px;font-weight:650;padding:2px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.06em;line-height:1.4;border:1px solid transparent}
162
- .badge-request{background:#eff6ff;color:var(--blue);border-color:#dbeafe}
163
- .badge-error{background:#fff1f2;color:var(--red);border-color:#ffe4e6}
164
- .badge-usecase{background:#f5f3ff;color:var(--purple);border-color:#ede9fe}
165
- .badge-eventBus{background:#ecfdf5;color:var(--green);border-color:#d1fae5}
166
- .badge-job{background:#fff7ed;color:var(--orange);border-color:#fed7aa}
167
- .badge-outbox{background:#fefce8;color:#a16207;border-color:#fef08a}
168
- .badge-schedule{background:#f0f9ff;color:#0369a1;border-color:#bae6fd}
169
- .badge-provider{background:#f8fafc;color:var(--gray);border-color:var(--border)}
170
- .badge-custom{background:#f0fdfa;color:#0f766e;border-color:#ccfbf1}
171
- .badge-auth{background:#eef2ff;color:var(--accent-strong);border-color:#c7d2fe}
172
- .badge-audit{background:#fef2f2;color:#b91c1c;border-color:#fecaca}
173
- .badge-cache{background:#f7fee7;color:#4d7c0f;border-color:#d9f99d}
174
- .badge-db{background:#ecfeff;color:#0e7490;border-color:#cffafe}
175
- .badge-mail{background:#fdf2f8;color:#be185d;border-color:#fbcfe8}
176
- .badge-notifications{background:#f5f3ff;color:#6d28d9;border-color:#ddd6fe}
177
- .badge-rateLimit{background:#fffbeb;color:#b45309;border-color:#fde68a}
178
- .badge-storage{background:#f0fdf4;color:#15803d;border-color:#bbf7d0}
179
- .badge-uploads{background:#f0f9ff;color:#0369a1;border-color:#bae6fd}
180
- .request-id{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);padding:2px 5px;background:var(--surface-2);border:1px solid var(--border);border-radius:4px}
181
- .trace-id{font-family:var(--font-mono);font-size:10px;color:var(--accent-strong);padding:2px 5px;background:#eef2ff;border:1px solid rgba(79,70,229,.16);border-radius:4px}
182
- @media(max-width:760px){
183
- .shell{padding:18px}
184
- header{flex-direction:column;align-items:stretch}
185
- .controls{justify-content:flex-start}
186
- .stats{grid-template-columns:repeat(2,minmax(0,1fr))}
187
- .panel-top{align-items:stretch;flex-direction:column}
188
- .toolbar input,.toolbar select{width:100%}
189
- .toolbar{align-items:stretch;flex-direction:column}
190
- .event-header{align-items:flex-start;flex-wrap:wrap}
191
- .event-time{min-width:0}
192
- .event-summary{white-space:normal}
193
- .request-detail{grid-template-columns:1fr}
194
- .request-map{grid-template-columns:repeat(2,minmax(0,1fr))}
195
- .detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
196
- .phase-row{grid-template-columns:1fr;gap:5px}
197
- .focus-head{flex-direction:column}
198
- .focus-metrics{grid-template-columns:repeat(2,minmax(0,1fr));min-width:0;width:100%}
199
- .table-row{grid-template-columns:1fr;gap:6px}
200
- .error-owner-strip{grid-template-columns:repeat(2,minmax(0,1fr))}
201
- .error-row{grid-template-columns:1fr;gap:6px}
202
- .table-detail{grid-column:auto}
43
+ return DEVTOOLS_DASHBOARD_HTML.replace(
44
+ BASE_PATH_PLACEHOLDER,
45
+ () => escapedBasePath,
46
+ );
203
47
  }
204
- </style>
205
- </head>
206
- <body>
207
- <div class="shell">
208
- <header>
209
- <div>
210
- <div class="brand"><span class="mark">B</span><span>Beignet</span></div>
211
- <h1>Beignet <span>Devtools</span></h1>
212
- <div class="subtitle">A local timeline for requests, use cases, errors, jobs, outbox delivery, schedules, and provider activity.</div>
213
- </div>
214
- <div class="controls">
215
- <span class="status"><span class="dot" id="statusDot"></span><span id="statusText">Connecting</span></span>
216
- <button id="togglePause">Pause</button>
217
- <button class="danger" id="clearBtn">Clear</button>
218
- </div>
219
- </header>
220
- <div class="stats">
221
- <div class="stat"><div class="stat-label">Events</div><div class="stat-value" id="statTotal">0</div></div>
222
- <div class="stat"><div class="stat-label">Requests</div><div class="stat-value" id="statRequests">0</div></div>
223
- <div class="stat"><div class="stat-label">Errors</div><div class="stat-value" id="statErrors">0</div></div>
224
- <div class="stat"><div class="stat-label">Use cases</div><div class="stat-value" id="statUseCases">0</div></div>
225
- </div>
226
- <section class="panel" aria-label="Devtools event timeline">
227
- <div class="panel-top">
228
- <nav class="tabs" id="tabs" aria-label="Event type filters"></nav>
229
- <div class="toolbar">
230
- <input type="search" id="searchInput" placeholder="Search events, paths, messages, details" />
231
- <select id="methodFilter" aria-label="HTTP method filter">
232
- <option value="">All methods</option>
233
- <option value="GET">GET</option>
234
- <option value="POST">POST</option>
235
- <option value="PUT">PUT</option>
236
- <option value="PATCH">PATCH</option>
237
- <option value="DELETE">DELETE</option>
238
- <option value="HEAD">HEAD</option>
239
- <option value="OPTIONS">OPTIONS</option>
240
- </select>
241
- <select id="statusFilter" aria-label="HTTP status filter">
242
- <option value="">All statuses</option>
243
- <option value="2xx">2xx</option>
244
- <option value="3xx">3xx</option>
245
- <option value="4xx">4xx</option>
246
- <option value="5xx">5xx</option>
247
- </select>
248
- <select id="watcherFilter" aria-label="Watcher filter">
249
- <option value="">All watchers</option>
250
- </select>
251
- <button id="resetFilters" type="button">Reset</button>
252
- </div>
253
- </div>
254
- <main id="content" class="event-list"></main>
255
- </section>
256
- </div>
257
- <script>
258
- (function(){
259
- const BASE="${sanitizedBasePath}";
260
- const TABS=[
261
- {id:"timeline",label:"Timeline"},
262
- {id:"requests",label:"Requests",type:"request",watcher:"requests"},
263
- {id:"usecases",label:"Use cases",type:"usecase",watcher:"useCases"},
264
- {id:"errors",label:"Errors",type:"error",watcher:"errors"},
265
- {id:"events",label:"Events",type:"eventBus",watcher:"eventBus"},
266
- {id:"jobs",label:"Jobs",type:"job",watcher:"jobs"},
267
- {id:"outbox",label:"Outbox",watcher:"outbox",watcherName:"outbox"},
268
- {id:"schedules",label:"Schedules",type:"schedule",watcher:"schedules"},
269
- {id:"providers",label:"Providers",type:"provider",watcher:"providers"},
270
- {id:"db",label:"Database",type:"custom",watcher:"db",watcherName:"db"},
271
- {id:"cache",label:"Cache",type:"custom",watcher:"cache",watcherName:"cache"},
272
- {id:"storage",label:"Storage",type:"custom",watcher:"storage",watcherName:"storage"},
273
- {id:"uploads",label:"Uploads",type:"custom",watcher:"uploads",watcherName:"uploads"},
274
- {id:"mail",label:"Mail",type:"custom",watcher:"mail",watcherName:"mail"},
275
- {id:"notifications",label:"Notifications",type:"custom",watcher:"notifications",watcherName:"notifications"},
276
- {id:"auth",label:"Auth",type:"custom",watcher:"auth",watcherName:"auth"},
277
- {id:"audit",label:"Audit",type:"custom",watcher:"audit",watcherName:"audit"},
278
- {id:"rateLimit",label:"Rate limits",type:"custom",watcher:"rateLimit",watcherName:"rateLimit"},
279
- {id:"custom",label:"Custom",type:"custom",watcher:"custom"}
280
- ];
281
- const FOCUS_PANEL_TABS={events:true,jobs:true,outbox:true,schedules:true,db:true,cache:true,storage:true,uploads:true,mail:true,notifications:true,auth:true,audit:true,rateLimit:true};
282
- const BUILT_IN_WATCHERS={requests:true,errors:true,useCases:true,eventBus:true,jobs:true,outbox:true,schedules:true,providers:true,db:true,cache:true,storage:true,uploads:true,mail:true,notifications:true,auth:true,audit:true,rateLimit:true,custom:true};
283
- const ERROR_OWNERS=["route","framework","provider","job","schedule","outbox","client","devtools","unknown"];
284
- let events=[];
285
- let watchers=[];
286
- let activeTab=localStorage.getItem("beignet-devtools-tab")||"timeline";
287
- let paused=localStorage.getItem("beignet-devtools-paused")==="true";
288
- let timer=null;
289
- let source=null;
290
-
291
- const $tabs=document.getElementById("tabs");
292
- const $content=document.getElementById("content");
293
- const $toggle=document.getElementById("togglePause");
294
- const $clear=document.getElementById("clearBtn");
295
- const $search=document.getElementById("searchInput");
296
- const $methodFilter=document.getElementById("methodFilter");
297
- const $statusFilter=document.getElementById("statusFilter");
298
- const $watcherFilter=document.getElementById("watcherFilter");
299
- const $resetFilters=document.getElementById("resetFilters");
300
- const $dot=document.getElementById("statusDot");
301
- const $statusText=document.getElementById("statusText");
302
- const $statTotal=document.getElementById("statTotal");
303
- const $statRequests=document.getElementById("statRequests");
304
- const $statErrors=document.getElementById("statErrors");
305
- const $statUseCases=document.getElementById("statUseCases");
306
-
307
- function esc(value){
308
- if(value===undefined||value===null)return"";
309
- return String(value).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
310
- }
311
-
312
- function json(value){
313
- return esc(JSON.stringify(value,null,2));
314
- }
315
-
316
- function jsonSearch(value){
317
- if(value===undefined)return"";
318
- try{return JSON.stringify(value)}
319
- catch{return String(value)}
320
- }
321
-
322
- function tabFor(id){
323
- return visibleTabs().find(function(tab){return tab.id===id})||TABS[0];
324
- }
325
-
326
- function watcherEnabled(name){
327
- const watcher=watchers.find(function(item){return item.name===name});
328
- return watcher?watcher.enabled:true;
329
- }
330
-
331
- function visibleTabs(){
332
- return TABS.filter(function(tab){return !tab.watcher||watcherEnabled(tab.watcher)}).concat(customWatcherTabs());
333
- }
334
-
335
- function customWatcherTabs(){
336
- return watchers.filter(function(watcher){
337
- return !BUILT_IN_WATCHERS[watcher.name]&&watcher.enabled&&watcher.eventTypes&&watcher.eventTypes.indexOf("custom")>=0;
338
- }).map(function(watcher){
339
- return {id:"watcher:"+watcher.name,label:watcher.label,type:"custom",watcherName:watcher.name};
340
- });
341
- }
342
-
343
- function summarize(e){
344
- switch(e.type){
345
- case "request": return (e.method||"")+" "+(e.path||"")+(e.status?" -> "+e.status:"")+(e.durationMs!==undefined?" ("+e.durationMs+"ms)":"");
346
- case "error": return e.message||"Unknown error";
347
- case "usecase": return e.name+" ("+e.phase+")"+(e.durationMs!==undefined?" "+e.durationMs+"ms":"");
348
- case "eventBus": return e.eventName||"Domain event";
349
- case "job": return e.jobName+" -> "+e.status;
350
- case "outbox": return e.messageName+" ("+e.messageKind+") -> "+e.status;
351
- case "schedule": return e.scheduleName+" -> "+e.status;
352
- case "provider": return e.providerName+" -> "+e.action;
353
- case "custom": return (e.label||e.name||"Custom event")+(e.summary?" - "+e.summary:"");
354
- default: return "";
355
- }
356
- }
357
-
358
- function tabMatchesEvent(tab,event){
359
- if(tab.id==="errors")return event.type==="error"||failureFor(event);
360
- if(tab.type&&event.type!==tab.type)return false;
361
- if(tab.watcherName&&event.watcher!==tab.watcherName)return false;
362
- return true;
363
- }
364
-
365
- function inferredWatcher(event){
366
- if(event.watcher)return event.watcher;
367
- switch(event.type){
368
- case "request": return "requests";
369
- case "error": return "errors";
370
- case "usecase": return "useCases";
371
- case "eventBus": return "eventBus";
372
- case "job": return "jobs";
373
- case "outbox": return "outbox";
374
- case "schedule": return "schedules";
375
- case "provider": return "providers";
376
- case "custom": return "custom";
377
- default: return "";
378
- }
379
- }
380
-
381
- function eventBadgeLabel(event){
382
- if(event.type==="custom"){
383
- switch(inferredWatcher(event)){
384
- case "auth": return "auth";
385
- case "audit": return "audit";
386
- case "cache": return "cache";
387
- case "db": return "database";
388
- case "mail": return "mail";
389
- case "notifications": return "notifications";
390
- case "rateLimit": return "rate limit";
391
- case "storage": return "storage";
392
- case "uploads": return "uploads";
393
- default: return "custom";
394
- }
395
- }
396
- return event.type;
397
- }
398
-
399
- function eventBadgeClass(event){
400
- if(event.type==="custom"){
401
- switch(inferredWatcher(event)){
402
- case "auth": return "auth";
403
- case "audit": return "audit";
404
- case "cache": return "cache";
405
- case "db": return "db";
406
- case "mail": return "mail";
407
- case "notifications": return "notifications";
408
- case "rateLimit": return "rateLimit";
409
- case "storage": return "storage";
410
- case "uploads": return "uploads";
411
- default: return "custom";
412
- }
413
- }
414
- return event.type;
415
- }
416
-
417
- function eventBadge(event){
418
- return '<span class="badge badge-'+esc(eventBadgeClass(event))+'">'+esc(eventBadgeLabel(event))+'</span>';
419
- }
420
-
421
- function statusGroup(status){
422
- const value=Number(status);
423
- if(!Number.isFinite(value))return"";
424
- if(value>=200&&value<300)return"2xx";
425
- if(value>=300&&value<400)return"3xx";
426
- if(value>=400&&value<500)return"4xx";
427
- if(value>=500&&value<600)return"5xx";
428
- return"";
429
- }
430
-
431
- function eventSearchText(event){
432
- return [
433
- event.id,
434
- event.type,
435
- event.requestId,
436
- event.traceId,
437
- event.watcher,
438
- inferredWatcher(event),
439
- summarize(event),
440
- event.method,
441
- event.path,
442
- event.status,
443
- event.contractName,
444
- event.useCaseName,
445
- event.message,
446
- event.name,
447
- event.label,
448
- event.summary,
449
- event.eventName,
450
- event.jobName,
451
- event.messageId,
452
- event.messageKind,
453
- event.messageName,
454
- event.providerName,
455
- event.action,
456
- jsonSearch(event.details)
457
- ].filter(Boolean).join(" ").toLowerCase();
458
- }
459
48
 
460
- function count(tab){
461
- if(!tab||tab.id==="timeline")return events.length;
462
- return events.filter(function(event){return tabMatchesEvent(tab,event)}).length;
463
- }
464
-
465
- function filteredEvents(){
466
- const tab=tabFor(activeTab);
467
- const query=$search.value.trim().toLowerCase();
468
- const method=$methodFilter.value;
469
- const status=$statusFilter.value;
470
- const watcher=$watcherFilter.value;
471
- return events.filter(function(event){
472
- if(!tabMatchesEvent(tab,event))return false;
473
- if(query&&eventSearchText(event).indexOf(query)<0)return false;
474
- if(method&&event.method!==method)return false;
475
- if(status&&statusGroup(event.status)!==status)return false;
476
- if(watcher&&inferredWatcher(event)!==watcher)return false;
477
- return true;
478
- });
479
- }
480
-
481
- function correlatedEvents(request){
482
- return events.filter(function(event){
483
- if(event.id===request.id)return false;
484
- if(request.traceId&&event.traceId===request.traceId)return true;
485
- return Boolean(request.requestId)&&event.requestId===request.requestId;
486
- });
487
- }
488
-
489
- function eventTimeValue(event){
490
- const time=Date.parse(event.timestamp||"");
491
- return Number.isFinite(time)?time:0;
492
- }
493
-
494
- function responseOwnerFor(event){
495
- const details=detailsObject(event);
496
- const response=details.response&&typeof details.response==="object"?details.response:{};
497
- return event.responseOwner||event.owner||response.owner||"unknown";
498
- }
499
-
500
- function ownerClass(owner){
501
- return "owner-"+String(owner||"unknown").replace(/[^a-zA-Z0-9_-]/g,"");
502
- }
503
-
504
- function ownerPill(owner){
505
- const value=owner||"unknown";
506
- return '<span class="owner-pill '+esc(ownerClass(value))+'">'+esc(value)+'</span>';
507
- }
508
-
509
- function categoryFor(event){
510
- const watcher=inferredWatcher(event);
511
- if(event.type==="request")return"HTTP";
512
- if(event.type==="usecase")return"Use case";
513
- if(event.type==="error")return"Error";
514
- if(event.type==="provider")return"Provider";
515
- if(event.type==="eventBus")return"Domain event";
516
- if(event.type==="outbox")return"Outbox";
517
- if(event.type==="job")return"Job";
518
- if(event.type==="schedule")return"Schedule";
519
- if(["db","cache","storage","uploads","mail","notifications","auth","audit","rateLimit"].indexOf(watcher)>=0)return"Provider work";
520
- return watcher==="custom"?"Custom":"Provider work";
521
- }
522
-
523
- function ownerForEvent(event){
524
- if(event.owner)return event.owner;
525
- const watcher=inferredWatcher(event);
526
- if(event.type==="error")return responseOwnerFor(event);
527
- if(event.type==="provider")return"provider";
528
- if(event.type==="job")return"job";
529
- if(event.type==="schedule")return"schedule";
530
- if(event.type==="outbox")return"outbox";
531
- if(["db","cache","storage","uploads","mail","notifications","auth","audit","rateLimit"].indexOf(watcher)>=0)return"provider";
532
- if(event.type==="request")return responseOwnerFor(event);
533
- return"route";
534
- }
535
-
536
- function relatedEventsFor(event){
537
- return events.filter(function(candidate){
538
- if(candidate.id===event.id)return false;
539
- if(event.traceId&&candidate.traceId===event.traceId)return true;
540
- return Boolean(event.requestId)&&candidate.requestId===event.requestId;
541
- });
542
- }
543
-
544
- function requestForEvent(event){
545
- return relatedEventsFor(event).filter(function(candidate){return candidate.type==="request"}).sort(function(a,b){return eventTimeValue(b)-eventTimeValue(a)})[0];
546
- }
547
-
548
- function statusForEvent(event){
549
- if(event.status!==undefined)return event.status;
550
- const request=requestForEvent(event);
551
- if(request&&request.status!==undefined)return request.status;
552
- const details=detailsObject(event);
553
- const response=details.response&&typeof details.response==="object"?details.response:{};
554
- return response.status||"";
555
- }
556
-
557
- function contractForEvent(event){
558
- const request=requestForEvent(event);
559
- return event.contractName||event.useCaseName||(request&&request.contractName)||"";
560
- }
561
-
562
- function pathForEvent(event){
563
- const request=requestForEvent(event);
564
- return event.path||(request&&request.path)||"";
565
- }
566
-
567
- function correlationFor(event){
568
- const rid=event.requestId?'<span class="request-id">'+esc(event.requestId)+'</span>':"";
569
- const trace=event.traceId?'<span class="trace-id">trace '+esc(event.traceId.slice(0,8))+'</span>':"";
570
- return rid+trace||'<span class="table-empty">n/a</span>';
571
- }
572
-
573
- function unsafeMetadataPaths(value,path){
574
- if(!value||typeof value!=="object")return[];
575
- return Object.entries(value).flatMap(function(entry){
576
- const key=entry[0];
577
- const nested=entry[1];
578
- const next=path.concat(key);
579
- const lower=key.toLowerCase();
580
- const sensitive=lower==="authorization"||lower==="cookie"||lower==="set-cookie"||lower.indexOf("token")>=0||lower.indexOf("password")>=0||lower.indexOf("secret")>=0||lower.indexOf("credential")>=0;
581
- if(sensitive&&nested!=="[redacted]")return[next.join(".")];
582
- return unsafeMetadataPaths(nested,next);
583
- });
584
- }
585
-
586
- function requestFact(label,value){
587
- const display=value===undefined||value===null||value===""?"n/a":value;
588
- return '<div class="request-fact"><div class="request-fact-label">'+esc(label)+'</div><div class="request-fact-value" title="'+esc(display)+'">'+esc(display)+'</div></div>';
589
- }
590
-
591
- function detailGrid(items){
592
- return '<div class="detail-grid">'+items.map(function(item){return requestFact(item[0],item[1])}).join("")+'</div>';
593
- }
594
-
595
- function requestOverview(request,related){
596
- return '<div class="request-map">'+
597
- requestFact("Route",request.contractName||"unmatched")+
598
- requestFact("Method",request.method||"")+
599
- requestFact("Path",request.path||"")+
600
- requestFact("Status",request.status||"")+
601
- requestFact("Duration",request.durationMs!==undefined?request.durationMs+"ms":"")+
602
- requestFact("Owner",responseOwnerFor(request))+
603
- requestFact("Trace",request.traceId||"")+
604
- requestFact("Related",related.length)+
605
- '</div>';
606
- }
607
-
608
- function requestLifecycleRows(request,related){
609
- const rows=[request].concat(related).sort(function(a,b){return eventTimeValue(a)-eventTimeValue(b)});
610
- return rows.map(function(event){
611
- const duration=durationFor(event);
612
- const durationLabel=duration===undefined?"":duration+"ms";
613
- return '<div class="phase-row">'+
614
- '<div>'+eventBadge(event)+'</div>'+
615
- '<div><strong class="phase-name">'+esc(categoryFor(event))+'</strong><div class="phase-meta">'+esc(durationLabel||inferredWatcher(event))+'</div></div>'+
616
- '<div class="phase-summary" title="'+esc(summarize(event))+'">'+esc(summarize(event))+'</div>'+
617
- '<div>'+ownerPill(ownerForEvent(event))+'</div>'+
618
- '</div>';
619
- }).join("");
620
- }
621
-
622
- function groupedRelatedRows(related){
623
- const groups=["Use case","Provider work","Domain event","Outbox","Job","Schedule","Error","Provider","Custom"];
624
- return groups.map(function(group){
625
- const groupEvents=related.filter(function(event){return categoryFor(event)===group});
626
- if(!groupEvents.length)return"";
627
- const rows=groupEvents.slice().sort(function(a,b){return eventTimeValue(a)-eventTimeValue(b)}).map(function(event){
628
- return '<div class="correlated-row">'+eventBadge(event)+'<span>'+esc(summarize(event))+'</span>'+ownerPill(ownerForEvent(event))+'</div>';
629
- }).join("");
630
- return '<section class="detail-section"><div class="detail-title">'+esc(group)+'</div><div class="correlated">'+rows+'</div></section>';
631
- }).join("");
632
- }
633
-
634
- function redactionNote(request,related){
635
- const all=[request].concat(related);
636
- const redacted=all.some(function(event){return hasRedactedValue(event)});
637
- const unsafe=Array.from(new Set(all.flatMap(function(event){return unsafeMetadataPaths(event,[])}))).slice(0,4);
638
- if(unsafe.length){
639
- return '<div class="redaction-note">Unsafe-looking metadata keys remain visible: '+esc(unsafe.join(", "))+'</div>';
640
- }
641
- if(redacted){
642
- return '<div class="redaction-note">Sensitive metadata was redacted before storage.</div>';
643
- }
644
- return '<div class="redaction-note">No redacted fields detected in the stored events for this request.</div>';
645
- }
646
-
647
- function updateStats(){
648
- $statTotal.textContent=String(events.length);
649
- $statRequests.textContent=String(events.filter(function(event){return event.type==="request"}).length);
650
- $statErrors.textContent=String(events.filter(function(event){return event.type==="error"}).length);
651
- $statUseCases.textContent=String(events.filter(function(event){return event.type==="usecase"}).length);
652
- }
653
-
654
- function renderTabs(){
655
- const tabs=visibleTabs();
656
- if(!tabs.some(function(tab){return tab.id===activeTab})){
657
- activeTab="timeline";
658
- localStorage.setItem("beignet-devtools-tab",activeTab);
659
- }
660
- $tabs.innerHTML=tabs.map(function(tab){
661
- const active=tab.id===activeTab?" active":"";
662
- return '<button class="'+active+'" data-tab="'+esc(tab.id)+'">'+esc(tab.label)+'<span class="tab-count">'+count(tab)+'</span></button>';
663
- }).join("");
664
- document.querySelectorAll("[data-tab]").forEach(function(btn){
665
- btn.addEventListener("click",function(){
666
- activeTab=btn.dataset.tab;
667
- localStorage.setItem("beignet-devtools-tab",activeTab);
668
- render();
669
- });
670
- });
671
- }
672
-
673
- function renderWatcherFilter(){
674
- const selected=$watcherFilter.value;
675
- const names=new Map();
676
- watchers.forEach(function(watcher){
677
- if(watcher.enabled)names.set(watcher.name,watcher.label||watcher.name);
678
- });
679
- events.forEach(function(event){
680
- const watcher=inferredWatcher(event);
681
- if(watcher&&!names.has(watcher))names.set(watcher,watcher);
682
- });
683
- const options=['<option value="">All watchers</option>'].concat(
684
- Array.from(names.entries()).sort(function(a,b){return a[1].localeCompare(b[1])}).map(function(entry){
685
- return '<option value="'+esc(entry[0])+'">'+esc(entry[1])+'</option>';
686
- })
687
- );
688
- $watcherFilter.innerHTML=options.join("");
689
- if(selected&&names.has(selected)){
690
- $watcherFilter.value=selected;
691
- }
692
- }
693
-
694
- function eventRow(e, detail){
695
- const time=e.timestamp?new Date(e.timestamp).toLocaleTimeString():"";
696
- const rid=e.requestId?'<span class="request-id">'+esc(e.requestId)+'</span>':"";
697
- const trace=e.traceId?'<span class="trace-id">trace '+esc(e.traceId.slice(0,8))+'</span>':"";
698
- return '<article class="event" onclick="this.classList.toggle(\\'open\\')">'+
699
- '<div class="event-header">'+
700
- '<span class="event-time">'+esc(time)+'</span>'+
701
- eventBadge(e)+
702
- rid+
703
- trace+
704
- '<span class="event-summary">'+esc(summarize(e))+'</span>'+
705
- '</div>'+
706
- '<div class="event-detail">'+detail+'</div>'+
707
- '</article>';
708
- }
709
-
710
- function renderTimeline(list){
711
- return list.slice().reverse().map(function(event){
712
- return eventRow(event,json(event));
713
- }).join("");
714
- }
715
-
716
- function renderRequests(list){
717
- return list.slice().reverse().map(function(request){
718
- const related=correlatedEvents(request);
719
- const detail='<div class="request-detail">'+
720
- '<section class="detail-section"><div class="detail-title">Request lifecycle</div>'+requestOverview(request,related)+'<div class="lifecycle">'+requestLifecycleRows(request,related)+'</div>'+redactionNote(request,related)+'</section>'+
721
- '<section class="detail-section"><div class="detail-title">Correlated activity</div>'+(related.length?groupedRelatedRows(related):'<div class="empty">No correlated events for this request.</div>')+'<pre>'+json(request)+'</pre></section>'+
722
- '</div>';
723
- return eventRow(request,detail);
724
- }).join("");
725
- }
726
-
727
- function metric(label,value){
728
- return '<div class="focus-metric"><div class="focus-metric-label">'+esc(label)+'</div><div class="focus-metric-value">'+esc(value)+'</div></div>';
729
- }
730
-
731
- ${devtoolsPanelModelBrowserSource()}
732
-
733
- function summarize(e){return summarizeDevtoolsEvent(e);}
734
- function inferredWatcher(event){return inferDevtoolsWatcher(event);}
735
- function eventBadgeLabel(event){return devtoolsEventBadgeLabel(event);}
736
- function eventBadgeClass(event){return devtoolsEventBadgeClass(event);}
737
- function durationFor(event){return devtoolsEventDuration(event);}
738
- function providerFor(event){return devtoolsEventProvider(event);}
739
- function failureFor(event){return devtoolsEventFailure(event);}
740
- function hasRedactedValue(value){return hasDevtoolsRedactedValue(value);}
741
- function panelMeta(tab){return devtoolsPanelMeta(tab.id,tab.label);}
742
- function panelMetrics(tab,list){
743
- return devtoolsPanelMetrics(tab.id,list).map(function(item){
744
- return metric(item.label,item.value);
745
- }).join("");
746
- }
747
- function primaryFor(event){return devtoolsPanelPrimary(tabFor(activeTab).id,event);}
748
- function secondaryFor(event){return devtoolsPanelSecondary(tabFor(activeTab).id,event);}
749
- function resultFor(event){return devtoolsPanelResult(event);}
750
- function tableDetailFor(tab,event){
751
- const detail=devtoolsPanelDetail(tab.id,event);
752
- return detailGrid(detail.fields.map(function(item){return [item.label,item.value]}))+
753
- '<div class="table-detail-note">'+esc(detail.note)+'</div><pre>'+json(event)+'</pre>';
754
- }
755
-
756
- function resultChip(value){
757
- const normalized=String(value||"").replace(/[^a-zA-Z0-9_-]/g,"_");
758
- return '<span class="result-chip result-'+esc(normalized)+'">'+esc(value||"n/a")+'</span>';
759
- }
760
-
761
- function tableRow(tab,event){
762
- const time=event.timestamp?new Date(event.timestamp).toLocaleTimeString():"";
763
- const duration=durationFor(event);
764
- const durationLabel=duration===undefined?"":duration+"ms";
765
- const provider=providerFor(event);
766
- return '<article class="table-row" onclick="this.classList.toggle(\\'open\\')">'+
767
- '<div class="table-cell">'+esc(time)+'</div>'+
768
- '<div class="table-cell"><strong>'+esc(primaryFor(event))+'</strong><code>'+esc(secondaryFor(event))+'</code></div>'+
769
- '<div class="table-cell table-summary">'+esc(summarize(event))+'</div>'+
770
- '<div class="table-cell table-correlation">'+correlationFor(event)+'</div>'+
771
- '<div class="table-cell">'+esc(durationLabel)+'</div>'+
772
- '<div class="table-cell">'+resultChip(resultFor(event)||provider)+'</div>'+
773
- '<div class="table-detail">'+tableDetailFor(tab,event)+'</div>'+
774
- '</article>';
775
- }
776
-
777
- function renderFocusPanel(tab,list){
778
- const meta=panelMeta(tab);
779
- const rows=list.slice().reverse().map(function(event){return tableRow(tab,event)}).join("");
780
- return '<section class="focus-panel">'+
781
- '<div class="focus-head">'+
782
- '<div><h2 class="focus-title">'+esc(meta.title)+'</h2><div class="focus-subtitle">'+esc(meta.subtitle)+'</div></div>'+
783
- '<div class="focus-metrics">'+panelMetrics(tab,list)+'</div>'+
784
- '</div>'+
785
- '<div class="event-table">'+rows+'</div>'+
786
- '</section>';
787
- }
788
-
789
- function renderErrorOwners(list){
790
- const owners=ERROR_OWNERS.map(function(owner){
791
- return {owner:owner,count:list.filter(function(event){return ownerForEvent(event)===owner}).length};
792
- }).filter(function(item){return item.count>0});
793
- const cards=(owners.length?owners:[{owner:"none",count:0}]).map(function(item){
794
- return '<div class="error-owner-card"><strong>'+esc(item.count)+'</strong><span>'+esc(item.owner)+'</span></div>';
795
- }).join("");
796
- return '<div class="error-owner-strip">'+cards+'</div>';
797
- }
798
-
799
- function renderErrorRelated(event){
800
- const related=relatedEventsFor(event).filter(function(candidate){return candidate.type!=="error"}).slice().sort(function(a,b){return eventTimeValue(a)-eventTimeValue(b)}).slice(0,8);
801
- if(!related.length)return '<div class="empty">No related request, provider, or side-effect events.</div>';
802
- return '<div class="error-related">'+related.map(function(candidate){
803
- return '<div class="correlated-row">'+eventBadge(candidate)+'<span>'+esc(summarize(candidate))+'</span>'+ownerPill(ownerForEvent(candidate))+'</div>';
804
- }).join("")+'</div>';
805
- }
806
-
807
- function failureMessageFor(event){
808
- if(event.message)return event.message;
809
- const details=detailsObject(event);
810
- const detailError=details.error;
811
- if(typeof detailError==="string"&&detailError)return detailError;
812
- if(detailError&&typeof detailError==="object"){
813
- if(typeof detailError.message==="string")return detailError.message;
814
- return jsonSearch(detailError);
815
- }
816
- return summarize(event)||"Unknown failure";
817
- }
818
-
819
- function renderErrorRow(event){
820
- const time=event.timestamp?new Date(event.timestamp).toLocaleTimeString():"";
821
- const status=statusForEvent(event);
822
- const route=[contractForEvent(event),pathForEvent(event)].filter(Boolean).join(" ");
823
- const relatedCount=relatedEventsFor(event).length;
824
- const message=failureMessageFor(event);
825
- return '<article class="error-row" onclick="this.classList.toggle(\\'open\\')">'+
826
- '<div class="table-cell">'+esc(time)+'</div>'+
827
- '<div><div class="error-message" title="'+esc(message)+'">'+esc(message)+'</div><div class="error-context">'+esc(route||event.useCaseName||primaryFor(event)||"unmatched")+'</div></div>'+
828
- '<div>'+ownerPill(ownerForEvent(event))+'</div>'+
829
- '<div class="table-cell table-correlation">'+correlationFor(event)+'</div>'+
830
- '<div class="table-cell">'+resultChip(status||relatedCount+" related")+'</div>'+
831
- '<div class="table-detail">'+renderErrorRelated(event)+'<pre>'+json(event)+'</pre></div>'+
832
- '</article>';
833
- }
834
-
835
- function renderErrorsPanel(list){
836
- const rowsByOwner=ERROR_OWNERS.map(function(owner){
837
- const ownerEvents=list.filter(function(event){return ownerForEvent(event)===owner});
838
- if(!ownerEvents.length)return"";
839
- const rows=ownerEvents.slice().reverse().map(renderErrorRow).join("");
840
- return '<section class="error-group">'+
841
- '<div class="error-group-head"><div class="error-group-title">'+ownerPill(owner)+'<span>'+esc(owner)+' failures</span></div><div class="error-group-count">'+esc(ownerEvents.length)+' errors</div></div>'+
842
- rows+
843
- '</section>';
844
- }).join("");
845
- return '<section class="error-board">'+renderErrorOwners(list)+rowsByOwner+'</section>';
846
- }
847
-
848
- function render(){
849
- updateStats();
850
- renderTabs();
851
- renderWatcherFilter();
852
- $toggle.textContent=paused?"Resume":"Pause";
853
- $dot.classList.toggle("paused",paused);
854
- const list=filteredEvents();
855
- if(!list.length){
856
- $content.innerHTML='<div class="empty">No events match this view.</div>';
857
- return;
858
- }
859
- const tab=tabFor(activeTab);
860
- if(activeTab==="requests"){
861
- $content.innerHTML=renderRequests(list);
862
- return;
863
- }
864
- if(activeTab==="errors"){
865
- $content.innerHTML=renderErrorsPanel(list);
866
- return;
867
- }
868
- if(FOCUS_PANEL_TABS[tab.id]){
869
- $content.innerHTML=renderFocusPanel(tab,list);
870
- return;
871
- }
872
- $content.innerHTML=renderTimeline(list);
873
- }
874
-
875
- function setStatus(text, pausedDot){
876
- $statusText.textContent=text;
877
- $dot.classList.toggle("paused",Boolean(pausedDot)||paused);
878
- }
879
-
880
- async function fetchEvents(){
881
- try{
882
- const res=await fetch(BASE+"/core/events?limit=500");
883
- if(!res.ok)return;
884
- const data=await res.json();
885
- events=data.events||[];
886
- watchers=data.watchers||watchers;
887
- setStatus("Polling",false);
888
- if(!paused)render();
889
- }catch(error){
890
- setStatus("Disconnected",true);
891
- }
892
- }
893
-
894
- function startPolling(){
895
- fetchEvents();
896
- timer=setInterval(fetchEvents,1500);
897
- }
898
-
899
- function connectStream(){
900
- if(!("EventSource" in window)){
901
- startPolling();
902
- return;
903
- }
904
- source=new EventSource(BASE+"/stream");
905
- source.onopen=function(){setStatus("Live",false)};
906
- source.addEventListener("snapshot",function(message){
907
- const data=JSON.parse(message.data);
908
- events=data.events||[];
909
- watchers=data.watchers||watchers;
910
- if(!paused)render();
911
- });
912
- source.addEventListener("event",function(message){
913
- const event=JSON.parse(message.data).event;
914
- events=events.concat(event).slice(-500);
915
- if(!paused)render();
916
- });
917
- source.addEventListener("clear",function(){
918
- events=[];
919
- if(!paused)render();
920
- });
921
- source.onerror=function(){setStatus("Reconnecting",true)};
922
- }
923
-
924
- $toggle.addEventListener("click",function(){
925
- paused=!paused;
926
- localStorage.setItem("beignet-devtools-paused",String(paused));
927
- if(!paused)render();
928
- else setStatus("Paused",true);
929
- });
930
-
931
- $clear.addEventListener("click",async function(){
932
- await fetch(BASE+"/clear",{method:"POST"});
933
- if(!source)await fetchEvents();
934
- });
935
-
936
- let searchTimer=null;
937
- function scheduleRender(){
938
- clearTimeout(searchTimer);
939
- searchTimer=setTimeout(render,120);
940
- }
941
- $search.addEventListener("input",scheduleRender);
942
- $methodFilter.addEventListener("change",render);
943
- $statusFilter.addEventListener("change",render);
944
- $watcherFilter.addEventListener("change",render);
945
- $resetFilters.addEventListener("click",function(){
946
- $search.value="";
947
- $methodFilter.value="";
948
- $statusFilter.value="";
949
- $watcherFilter.value="";
950
- render();
951
- });
952
-
953
- render();
954
- connectStream();
955
- window.addEventListener("beforeunload",function(){
956
- if(source)source.close();
957
- if(timer)clearInterval(timer);
958
- });
959
- })();
960
- </script>
961
- </body>
962
- </html>`;
49
+ /**
50
+ * Escape a value for a JSON string embedded in an HTML script tag. HTML-risky
51
+ * characters use JSON unicode escapes so the payload stays valid JSON.
52
+ */
53
+ function escapeForJsonScript(value: string): string {
54
+ return value
55
+ .replace(/\\/g, "\\\\")
56
+ .replace(/"/g, '\\"')
57
+ .replace(/&/g, "\\u0026")
58
+ .replace(/</g, "\\u003c")
59
+ .replace(/>/g, "\\u003e");
963
60
  }