@jsenv/core 41.5.35 → 41.5.38

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.
@@ -1,651 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Server clients inspector</title>
7
- <style>body {
8
- color: #111827;
9
- background: #f3f4f6;
10
- margin: 0;
11
- padding: 24px;
12
- font: 14px / 1.5 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
13
- }
14
-
15
- h1 {
16
- margin: 0 0 4px;
17
- font-size: 1.4rem;
18
- }
19
-
20
- h2 {
21
- color: #6b7280;
22
- text-transform: uppercase;
23
- letter-spacing: .04em;
24
- margin: 26px 0 8px;
25
- font-size: .95rem;
26
- }
27
-
28
- p.intro {
29
- color: #6b7280;
30
- margin: 0 0 8px;
31
- }
32
-
33
- table {
34
- border-collapse: collapse;
35
- background: #fff;
36
- border-radius: 8px;
37
- width: 100%;
38
- overflow: hidden;
39
- box-shadow: 0 1px 3px #0000001a;
40
- }
41
-
42
- th, td {
43
- text-align: left;
44
- vertical-align: middle;
45
- border-bottom: 1px solid #eef2f7;
46
- padding: 10px 14px;
47
- }
48
-
49
- th {
50
- color: #6b7280;
51
- text-transform: uppercase;
52
- letter-spacing: .04em;
53
- background: #f9fafb;
54
- font-size: .78rem;
55
- }
56
-
57
- tr:last-child td {
58
- border-bottom: none;
59
- }
60
-
61
- .empty {
62
- color: #9ca3af;
63
- text-align: center;
64
- padding: 22px;
65
- }
66
-
67
- .muted {
68
- color: #9ca3af;
69
- }
70
-
71
- .when {
72
- color: #374151;
73
- white-space: nowrap;
74
- }
75
-
76
- .url {
77
- color: #374151;
78
- text-align: left;
79
- text-overflow: ellipsis;
80
- white-space: nowrap;
81
- vertical-align: middle;
82
- direction: rtl;
83
- max-width: 240px;
84
- display: inline-block;
85
- overflow: hidden;
86
- }
87
-
88
- .tag {
89
- color: #4338ca;
90
- background: #eef2ff;
91
- border-radius: 999px;
92
- padding: 1px 7px;
93
- font-size: .72rem;
94
- font-weight: 600;
95
- display: inline-block;
96
- }
97
-
98
- .tag.on {
99
- color: #047857;
100
- background: #d1fae5;
101
- }
102
-
103
- .tag.ellip {
104
- text-overflow: ellipsis;
105
- white-space: nowrap;
106
- vertical-align: middle;
107
- max-width: 320px;
108
- overflow: hidden;
109
- }
110
-
111
- .actions {
112
- flex-wrap: wrap;
113
- align-items: center;
114
- gap: 6px;
115
- display: flex;
116
- }
117
-
118
- .btn {
119
- color: #374151;
120
- font: inherit;
121
- cursor: pointer;
122
- background: #f3f4f6;
123
- border: 1px solid #d1d5db;
124
- border-radius: 6px;
125
- padding: 2px 9px;
126
- font-size: .75rem;
127
- }
128
-
129
- .btn:hover {
130
- background: #e5e7eb;
131
- }
132
-
133
- .btn.primary {
134
- color: #fff;
135
- background: #2563eb;
136
- border-color: #2563eb;
137
- }
138
-
139
- .btn.primary:hover {
140
- background: #1d4ed8;
141
- }
142
-
143
- .monitor {
144
- color: #2563eb;
145
- white-space: nowrap;
146
- margin-left: 4px;
147
- padding: 2px 4px;
148
- font-size: .8rem;
149
- font-weight: 600;
150
- text-decoration: none;
151
- }
152
-
153
- .monitor:hover {
154
- text-decoration: underline;
155
- }
156
-
157
- dialog {
158
- border: none;
159
- border-radius: 10px;
160
- width: min(90vw, 640px);
161
- padding: 0;
162
- box-shadow: 0 20px 60px #0000004d;
163
- }
164
-
165
- dialog::backdrop {
166
- background: #0006;
167
- }
168
-
169
- .dlg-head {
170
- border-bottom: 1px solid #eef2f7;
171
- align-items: center;
172
- gap: 8px;
173
- padding: 12px 16px;
174
- display: flex;
175
- }
176
-
177
- .dlg-head .spacer {
178
- flex: 1;
179
- }
180
-
181
- .dlg-body {
182
- max-height: 60vh;
183
- padding: 14px 16px;
184
- overflow: auto;
185
- }
186
-
187
- .dlg-body pre {
188
- white-space: pre-wrap;
189
- word-break: break-all;
190
- margin: 0;
191
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
192
- font-size: 12px;
193
- }
194
-
195
- ul.list {
196
- margin: 0;
197
- padding: 0;
198
- list-style: none;
199
- }
200
-
201
- ul.list li {
202
- border-bottom: 1px solid #f1f5f9;
203
- padding: 8px 0;
204
- }
205
-
206
- ul.list li:last-child {
207
- border-bottom: none;
208
- }
209
-
210
- .url2 {
211
- color: #374151;
212
- word-break: break-all;
213
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
214
- font-size: 12px;
215
- }
216
-
217
- .ttl {
218
- font-weight: 600;
219
- }
220
- </style>
221
- </head>
222
- <body>
223
- <h1>Server clients inspector</h1>
224
- <p class="intro">
225
- Every browser that has connected to this dev server since it started. The
226
- columns are a résumé; use the actions to dig in or monitor a client's
227
- logs.
228
- </p>
229
-
230
- <h2>This browser</h2>
231
- <table>
232
- <thead id="head"></thead>
233
- <tbody id="self-rows"></tbody>
234
- </table>
235
-
236
- <h2>Other clients</h2>
237
- <table>
238
- <thead id="head2"></thead>
239
- <tbody id="other-rows"></tbody>
240
- </table>
241
-
242
- <h2>Headless browsers</h2>
243
- <p class="intro">
244
- Browsers driven by a program — a test run, a screenshot script — rather
245
- than by someone looking at them.
246
- </p>
247
- <table>
248
- <thead id="head3"></thead>
249
- <tbody id="headless-rows"></tbody>
250
- </table>
251
-
252
- <dialog id="dlg">
253
- <div class="dlg-head">
254
- <strong id="dlg-title"></strong>
255
- <span class="spacer"></span>
256
- <button class="btn" id="dlg-copy">Copy</button>
257
- <button class="btn" id="dlg-close">Close</button>
258
- </div>
259
- <div class="dlg-body" id="dlg-body"></div>
260
- </dialog>
261
-
262
- <script type="module">import { openPagePicker } from "../page_picker.js";
263
-
264
- const selfRows = document.getElementById("self-rows");
265
- const otherRows = document.getElementById("other-rows");
266
- const headlessRows = document.getElementById("headless-rows");
267
- // The monitoring script (injected into this page too) stores our own id.
268
- let selfId = null;
269
- try {
270
- selfId = localStorage.getItem("jsenv_client_id");
271
- } catch {
272
- // storage blocked — we just won't single ourselves out
273
- }
274
-
275
- const headHtml = `<tr>
276
- <th>First seen</th><th>OS</th><th>Browser</th><th>IP</th><th>Active tab</th>
277
- <th>Last activity</th><th>Logs</th><th></th>
278
- </tr>`;
279
- document.getElementById("head").innerHTML = headHtml;
280
- document.getElementById("head2").innerHTML = headHtml;
281
- document.getElementById("head3").innerHTML = headHtml;
282
- const COLSPAN = 8;
283
-
284
- const ago = (ts) => {
285
- const s = Math.round((Date.now() - ts) / 1000);
286
- if (s < 5) return "just now";
287
- if (s < 60) return `${s}s ago`;
288
- const m = Math.round(s / 60);
289
- if (m < 60) return `${m}m ago`;
290
- const h = Math.round(m / 60);
291
- return `${h}h ago`;
292
- };
293
- const pad2 = (n) => String(n).padStart(2, "0");
294
- // Absolute date+time, kept for the "first seen" tooltip.
295
- const fmtDate = (ts) => {
296
- const d = new Date(ts);
297
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
298
- };
299
- // Human relative time for "first seen" ("5 minutes ago", "yesterday"),
300
- // rendered with the largest unit that fits. Re-rendered every few seconds
301
- // (see the interval below) so it stays fresh.
302
- const relativeTimeFormat = new Intl.RelativeTimeFormat(undefined, {
303
- numeric: "auto",
304
- });
305
- const RELATIVE_UNITS = [
306
- ["year", 31536000],
307
- ["month", 2592000],
308
- ["week", 604800],
309
- ["day", 86400],
310
- ["hour", 3600],
311
- ["minute", 60],
312
- ];
313
- const relativeTime = (ts) => {
314
- const deltaSeconds = Math.round((ts - Date.now()) / 1000); // <0 = past
315
- const abs = deltaSeconds < 0 ? -deltaSeconds : deltaSeconds;
316
- for (const [unit, secondsInUnit] of RELATIVE_UNITS) {
317
- if (abs >= secondsInUnit) {
318
- return relativeTimeFormat.format(
319
- Math.round(deltaSeconds / secondsInUnit),
320
- unit,
321
- );
322
- }
323
- }
324
- return relativeTimeFormat.format(deltaSeconds, "second");
325
- };
326
- const escapeHtml = (str) =>
327
- String(str).replace(
328
- /[&<>"]/g,
329
- (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
330
- );
331
- // Trim noisy sub-pixel precision for display (e.g. scroll "0/432.333" →
332
- // "0/432"); the full value stays in the stored/reported data.
333
- const roundNumbers = (text) =>
334
- String(text).replace(/-?\d+\.\d+/g, (n) => String(Math.round(n)));
335
- const shortUrl = (url) => {
336
- try {
337
- const u = new URL(url);
338
- return `${u.pathname}${u.search}${u.hash}` || "/";
339
- } catch {
340
- return url;
341
- }
342
- };
343
- // Last path segment of a URL ("…/8_picker_wheel_demo.html" → the filename),
344
- // for compact activity labels. Non-URLs pass through unchanged.
345
- const fileName = (url) => {
346
- try {
347
- const segments = new URL(url).pathname.split("/").filter(Boolean);
348
- return segments.length ? segments[segments.length - 1] : "/";
349
- } catch {
350
- return url;
351
- }
352
- };
353
- // The compact form of an activity detail for the "Last activity" column:
354
- // URL-valued details collapse to their filename, a request keeps its
355
- // method, coordinates lose sub-pixel noise. The full detail stays in the
356
- // tooltip and the activity dialog / monitor.
357
- const compactDetail = (type, detail) => {
358
- if (!detail) {
359
- return "";
360
- }
361
- if (type === "load" || type === "navigation") {
362
- return fileName(detail);
363
- }
364
- if (type === "request") {
365
- const spaceIndex = detail.indexOf(" ");
366
- return spaceIndex === -1
367
- ? fileName(detail)
368
- : `${detail.slice(0, spaceIndex)} ${fileName(detail.slice(spaceIndex + 1))}`;
369
- }
370
- return roundNumbers(detail);
371
- };
372
- const RUNTIME_LABELS = {
373
- ios_safari: "iOS Safari",
374
- chrome: "Chrome",
375
- edge: "Edge",
376
- opera: "Opera",
377
- samsung: "Samsung Internet",
378
- firefox: "Firefox",
379
- safari: "Safari",
380
- node: "Node",
381
- };
382
- const named = ({ name, version } = {}) => {
383
- if (!name || name === "unknown") {
384
- return '<span class="muted">unknown</span>';
385
- }
386
- const label = RUNTIME_LABELS[name] || name;
387
- const v = version && version !== "unknown" ? ` ${version}` : "";
388
- return escapeHtml(`${label}${v}`);
389
- };
390
-
391
- const firstSeenCell = (d) =>
392
- `<span class="when" title="${escapeHtml(fmtDate(d.firstSeen))}">${escapeHtml(relativeTime(d.firstSeen))}</span>`;
393
- const tabCell = (d) => {
394
- if (!d.activeTab) {
395
- return '<span class="muted">—</span>';
396
- }
397
- const url = d.activeTab.url || "";
398
- return `<span class="url" title="${escapeHtml(url)}"><bdi dir="ltr">${escapeHtml(shortUrl(url))}</bdi></span>`;
399
- };
400
- const activityCell = (d) => {
401
- const a = d.lastActivity;
402
- if (!a) {
403
- return '<span class="muted">—</span>';
404
- }
405
- const compact = a.detail
406
- ? `${a.type}: ${compactDetail(a.type, a.detail)}`
407
- : a.type;
408
- const full = a.detail ? `${a.type}: ${a.detail}` : a.type;
409
- return `<span class="tag ellip" title="${escapeHtml(full)}">${escapeHtml(compact)}</span> <span class="muted">${ago(a.ts)}</span>`;
410
- };
411
- const actionsCell = (d) => {
412
- const id = escapeHtml(d.id);
413
- return `<div class="actions">
414
- <button class="btn primary" data-action="navigate" data-id="${id}">Navigate…</button>
415
- <button class="btn" data-action="reload" data-id="${id}">Reload</button>
416
- <button class="btn" data-action="tabs" data-id="${id}">Tabs</button>
417
- <button class="btn" data-action="activity" data-id="${id}">Activity</button>
418
- <button class="btn" data-action="ua" data-id="${id}">User agent</button>
419
- <a class="monitor" href="/.internal/client?id=${encodeURIComponent(d.id)}">Monitor →</a>
420
- </div>`;
421
- };
422
- // The ip says how the client reaches the server, which is what tells the
423
- // main client (this machine, via localhost) from a device on the network.
424
- const ipCell = (d) => {
425
- if (!d.ip) {
426
- return '<span class="muted">—</span>';
427
- }
428
- const label = d.local ? "localhost" : d.ip;
429
- return `<span title="${escapeHtml(d.ip)}">${escapeHtml(label)}</span>`;
430
- };
431
- const rowHtml = (d) => {
432
- return `<tr>
433
- <td>${firstSeenCell(d)}</td>
434
- <td>${named(d.os)}</td>
435
- <td>${named(d.runtime)}</td>
436
- <td>${ipCell(d)}</td>
437
- <td>${tabCell(d)}</td>
438
- <td>${activityCell(d)}</td>
439
- <td>${d.logCount}</td>
440
- <td>${actionsCell(d)}</td>
441
- </tr>`;
442
- };
443
-
444
- let lastClients = [];
445
- const render = (clients) => {
446
- lastClients = clients;
447
- const self = selfId ? clients.find((d) => d.id === selfId) : null;
448
- // The client someone is using right now comes first; among the quiet
449
- // ones, the newest. A client that never reported an activity sorts
450
- // after those that did.
451
- const byRecency = (a, b) => {
452
- const aActivity = a.lastActivity ? a.lastActivity.ts : 0;
453
- const bActivity = b.lastActivity ? b.lastActivity.ts : 0;
454
- if (aActivity !== bActivity) {
455
- return bActivity - aActivity;
456
- }
457
- return b.firstSeen - a.firstSeen;
458
- };
459
- // A headless browser is a program's, not a person's: it comes and goes
460
- // with a test run, so it gets its own table instead of pushing the
461
- // clients someone is actually browsing with out of sight.
462
- const others = clients
463
- .filter((d) => d !== self && !d.headless)
464
- .sort(byRecency);
465
- const headless = clients
466
- .filter((d) => d !== self && d.headless)
467
- .sort(byRecency);
468
- selfRows.innerHTML = self
469
- ? rowHtml(self)
470
- : `<tr><td class="empty" colspan="${COLSPAN}">This browser isn't registered yet — it will appear shortly.</td></tr>`;
471
- otherRows.innerHTML = others.length
472
- ? others.map(rowHtml).join("")
473
- : `<tr><td class="empty" colspan="${COLSPAN}">No other client has connected.</td></tr>`;
474
- headlessRows.innerHTML = headless.length
475
- ? headless.map(rowHtml).join("")
476
- : `<tr><td class="empty" colspan="${COLSPAN}">No headless browser has connected.</td></tr>`;
477
- };
478
-
479
- // Dialogs (all tabs / recent activity / user agent).
480
- const dlg = document.getElementById("dlg");
481
- const dlgTitle = document.getElementById("dlg-title");
482
- const dlgBody = document.getElementById("dlg-body");
483
- const dlgCopy = document.getElementById("dlg-copy");
484
- document.getElementById("dlg-close").onclick = () => dlg.close();
485
- const openDialog = (title, html, copyText) => {
486
- dlgTitle.textContent = title;
487
- dlgBody.innerHTML = html;
488
- if (copyText === undefined) {
489
- dlgCopy.style.display = "none";
490
- } else {
491
- dlgCopy.style.display = "";
492
- dlgCopy.textContent = "Copy";
493
- dlgCopy.onclick = async () => {
494
- try {
495
- await window.navigator.clipboard.writeText(copyText);
496
- dlgCopy.textContent = "Copied!";
497
- setTimeout(() => {
498
- dlgCopy.textContent = "Copy";
499
- }, 1200);
500
- } catch {
501
- // clipboard blocked — ignore
502
- }
503
- };
504
- }
505
- dlg.showModal();
506
- };
507
-
508
- const fetchClient = async (id) => {
509
- try {
510
- return await (
511
- await fetch(`/.internal/client.json?id=${encodeURIComponent(id)}`)
512
- ).json();
513
- } catch {
514
- return null;
515
- }
516
- };
517
- const tabsHtml = (tabs) => {
518
- if (!tabs.length) {
519
- return '<p class="muted">No tab.</p>';
520
- }
521
- return `<ul class="list">${tabs
522
- .slice()
523
- .sort((a, b) => b.visible - a.visible || b.lastSeen - a.lastSeen)
524
- .map(
525
- (t) => `<li>
526
- <div>${t.visible ? '<span class="tag on">active</span> ' : ""}<span class="ttl">${escapeHtml(t.title) || '<span class="muted">(no title)</span>'}</span> <span class="muted">${ago(t.lastSeen)}</span></div>
527
- <div class="url2">${escapeHtml(t.url)}</div>
528
- </li>`,
529
- )
530
- .join("")}</ul>`;
531
- };
532
- const activitiesHtml = (activities) => {
533
- if (!activities.length) {
534
- return '<p class="muted">No activity yet.</p>';
535
- }
536
- return `<ul class="list">${activities
537
- .slice()
538
- .reverse()
539
- .map(
540
- (a) => `<li>
541
- <div><span class="tag">${escapeHtml(a.type)}</span> <span class="muted">${ago(a.ts)}</span></div>
542
- ${a.detail ? `<div class="url2">${escapeHtml(roundNumbers(a.detail))}</div>` : ""}
543
- </li>`,
544
- )
545
- .join("")}</ul>`;
546
- };
547
-
548
- // Pilot a client: POST a command; the server broadcasts it and the
549
- // matching reporter runs it (navigate/reload) on the targeted tab.
550
- const sendCommand = async (payload) => {
551
- try {
552
- await fetch("/.internal/clients/command", {
553
- method: "POST",
554
- headers: { "content-type": "application/json" },
555
- body: JSON.stringify(payload),
556
- });
557
- } catch {
558
- // dev server gone — ignore
559
- }
560
- };
561
- const shortLabel = (d) => {
562
- const parts = [d.os && d.os.name, d.runtime && d.runtime.name].filter(
563
- Boolean,
564
- );
565
- return parts.length ? parts.join(" ") : d.id.slice(0, 8);
566
- };
567
- // Which page to send a client to is the same question cmd+K asks on any
568
- // dev-served page, so it is the same tree that answers it (see
569
- // protocol_file/client/page_picker.js) — filters, folding and all. Only
570
- // the ending differs: the url goes to the client's active tab (tabId from
571
- // activeTab, so no other tab moves) instead of to this window. No href on
572
- // the rows for that very reason: a click here must not take THIS browser
573
- // to a page meant for another one.
574
- const openNavigateDialog = (client) => {
575
- openPagePicker({
576
- title: `Navigate ${shortLabel(client)} to…`,
577
- placeholder: "Go to page…",
578
- hereUrl: client.activeTab ? client.activeTab.url : null,
579
- onPick: (page) => {
580
- sendCommand({
581
- clientId: client.id,
582
- tabId: client.activeTab ? client.activeTab.id : null,
583
- type: "navigate",
584
- url: page.url,
585
- });
586
- },
587
- });
588
- };
589
-
590
- document.addEventListener("click", async (event) => {
591
- const button = event.target.closest("button[data-action]");
592
- if (!button) {
593
- return;
594
- }
595
- const id = button.getAttribute("data-id");
596
- const action = button.getAttribute("data-action");
597
- if (action === "ua") {
598
- const client = lastClients.find((d) => d.id === id);
599
- const ua = (client && client.userAgent) || "";
600
- openDialog("User agent", `<pre>${escapeHtml(ua)}</pre>`, ua);
601
- return;
602
- }
603
- if (action === "navigate") {
604
- const client = lastClients.find((d) => d.id === id);
605
- if (client) {
606
- openNavigateDialog(client);
607
- }
608
- return;
609
- }
610
- if (action === "reload") {
611
- const client = lastClients.find((d) => d.id === id);
612
- sendCommand({
613
- clientId: id,
614
- tabId: client && client.activeTab ? client.activeTab.id : null,
615
- type: "reload",
616
- });
617
- return;
618
- }
619
- const detail = await fetchClient(id);
620
- if (!detail) {
621
- return;
622
- }
623
- if (action === "tabs") {
624
- openDialog(`Tabs (${detail.tabs.length})`, tabsHtml(detail.tabs));
625
- } else if (action === "activity") {
626
- openDialog("Recent activity", activitiesHtml(detail.activities));
627
- }
628
- });
629
-
630
- // This page is cooked through the dev server, so it gets both the jsenv
631
- // server-events client (window.__server_events__, injected first — the
632
- // same channel autoreload rides) and the monitoring script (so opening
633
- // this page counts as a connected client). The server pushes the full
634
- // list on every change; no polling. Initial snapshot via the JSON route.
635
- (async () => {
636
- try {
637
- render(await (await fetch("/.internal/clients.json")).json());
638
- } catch {
639
- // dev server unreachable — the live channel will fill the list in.
640
- }
641
- })();
642
-
643
- window.__server_events__.listenEvents({
644
- clients_list: (event) => render(event.data),
645
- });
646
-
647
- // Refresh relative "last activity" times between server pushes.
648
- setInterval(() => render(lastClients), 5000);
649
- </script>
650
- </body>
651
- </html>