@jsenv/core 41.4.1 → 41.4.3

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.
@@ -4,7 +4,7 @@ import { jsenvPluginMinification } from "@jsenv/plugin-minification";
4
4
  import { jsenvPluginTranspilation, jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
5
5
  import { memoryUsage } from "node:process";
6
6
  import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
7
- import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, urlIsOrIsInsideOf, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringCpuUsage, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
7
+ import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, urlIsOrIsInsideOf, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringCpuUsage, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
10
10
  import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
@@ -2286,6 +2286,17 @@ const createUrlInfo = (url, context) => {
2286
2286
  if (referenceFromOther.gotInlined()) {
2287
2287
  const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
2288
2288
  considerModified(urlInfoReferencingThisOne);
2289
+ continue;
2290
+ }
2291
+ // A reference with a versioning effect writes this url's VERSION into
2292
+ // its owner's cooked content (the ?v= param, read from package.json):
2293
+ // this url modified means that content now embeds a stale version, so
2294
+ // the owner is as modified as an owner of inlined content. Without
2295
+ // this, the owner's cooked content survives the modification and a
2296
+ // validity check that "heals" this url (see isValid re-reading files
2297
+ // from disk) leaves the graph claiming the owner is fresh.
2298
+ if (referenceFromOther.hasVersioningEffect) {
2299
+ considerModified(referenceFromOther.ownerUrlInfo);
2289
2300
  }
2290
2301
  }
2291
2302
  for (const searchParamVariant of urlInfo.searchParamVariantSet) {
@@ -3717,14 +3728,23 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
3717
3728
  if (!urlInfo.url.startsWith("ignore:")) {
3718
3729
  try {
3719
3730
  await urlInfo.dependencies.startCollecting(async () => {
3731
+ // Each phase timed into urlInfo.timing: the dev server turns it into
3732
+ // a server-timing response header, so devtools show where the time
3733
+ // to cook a file goes (fetch vs transform vs finalize).
3734
+ const timePhase = async (name, phase) => {
3735
+ const start = performance.now();
3736
+ await phase();
3737
+ urlInfo.timing[name] = performance.now() - start;
3738
+ };
3739
+
3720
3740
  // "fetchUrlContent" hook
3721
- await urlInfo.fetchContent();
3741
+ await timePhase("fetch", () => urlInfo.fetchContent());
3722
3742
 
3723
3743
  // "transform" hook
3724
- await urlInfo.transformContent();
3744
+ await timePhase("transform", () => urlInfo.transformContent());
3725
3745
 
3726
3746
  // "finalize" hook
3727
- await urlInfo.finalizeContent();
3747
+ await timePhase("finalize", () => urlInfo.finalizeContent());
3728
3748
  });
3729
3749
  } catch (e) {
3730
3750
  urlInfo.error = e;
@@ -6302,6 +6322,11 @@ const addRelationshipWithPackageJson = ({
6302
6322
  String(packageJsonContentAsBuffer),
6303
6323
  );
6304
6324
  }
6325
+ // Checked on disk at every validation rather than trusted to the watcher:
6326
+ // what this file decides (the package version, hence the ?v= importers
6327
+ // embed) ends up in the browser's immutable cache, so a request racing the
6328
+ // watcher must never be answered from a stale package.json.
6329
+ packageJsonReference.urlInfo.revalidateOnFileSystem = true;
6305
6330
  };
6306
6331
 
6307
6332
  const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
@@ -6527,6 +6552,127 @@ const jsenvPluginVersionSearchParam = () => {
6527
6552
  };
6528
6553
  };
6529
6554
 
6555
+ /*
6556
+ * The .html files under the served directory, as urls one can navigate to.
6557
+ *
6558
+ * Lives here, next to the plugin that owns the filesystem, because more than
6559
+ * one feature wants the same list: the client dashboard sends a browser to one
6560
+ * of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
6561
+ * gets the same answer.
6562
+ *
6563
+ * Each page comes with what kind of page it is, read from where it sits and
6564
+ * what it is called — the two conventions this repo already follows:
6565
+ * - "experiment": something tried out, under a lab/ directory or named
6566
+ * *_experiment.html;
6567
+ * - "demo": something shown, under a demos/ directory or named *_demo.html;
6568
+ * - "page": everything else.
6569
+ *
6570
+ * Scanned on demand rather than watched: the list is asked for when a human
6571
+ * opens a picker, which is rare and never on a hot path, and a short cache is
6572
+ * enough to keep a burst of asks from walking the tree twice.
6573
+ */
6574
+
6575
+
6576
+ const SCAN_TTL_MS = 5000;
6577
+
6578
+ // What to walk and what to skip, in the shape @jsenv/url-meta reads: the last
6579
+ // matching pattern wins, so the exclusions come after the "every .html" rule.
6580
+ // Dependencies, build output and jsenv's own caches hold no page worth going to.
6581
+ const HTML_PAGE_ASSOCIATIONS = {
6582
+ page: {
6583
+ "**/*.html": true,
6584
+ "**/.*/": false,
6585
+ "**/node_modules/": false,
6586
+ "**/dist/": false,
6587
+ "**/build/": false,
6588
+ "**/coverage/": false,
6589
+ "**/git_ignored/": false,
6590
+ "**/old/": false,
6591
+ },
6592
+ experiment: {
6593
+ "**/lab/**/*.html": true,
6594
+ "**/*_experiment.html": true,
6595
+ },
6596
+ demo: {
6597
+ "**/demos/**/*.html": true,
6598
+ "**/*_demo.html": true,
6599
+ },
6600
+ };
6601
+
6602
+ // An experiment inside a demos/ directory is an experiment: the more specific
6603
+ // of the two wins, and "shown" is the weaker claim.
6604
+ const readKind = (meta) => {
6605
+ if (meta.experiment) {
6606
+ return "experiment";
6607
+ }
6608
+ if (meta.demo) {
6609
+ return "demo";
6610
+ }
6611
+ return "page";
6612
+ };
6613
+
6614
+ // Which package a page belongs to: the nearest directory above it holding a
6615
+ // package.json, said as a url so whoever draws a tree can mark that very node.
6616
+ // Not the root itself — everything is under it, and "the whole repo" is not a
6617
+ // package one distinguishes from another. Memoized per directory: a scan asks
6618
+ // the same question once per file and there are hundreds of them.
6619
+ const createPackageDirectoryFinder = (rootDirectoryUrl) => {
6620
+ const cache = new Map();
6621
+ const find = (directoryUrl) => {
6622
+ if (cache.has(directoryUrl)) {
6623
+ return cache.get(directoryUrl);
6624
+ }
6625
+ let result = null;
6626
+ if (directoryUrl.length > String(rootDirectoryUrl).length) {
6627
+ result = existsSync(new URL("./package.json", directoryUrl))
6628
+ ? directoryUrl
6629
+ : find(new URL("../", directoryUrl).href);
6630
+ }
6631
+ cache.set(directoryUrl, result);
6632
+ return result;
6633
+ };
6634
+ return find;
6635
+ };
6636
+
6637
+ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
6638
+ let cache = null;
6639
+ let cachedAt = 0;
6640
+
6641
+ return async () => {
6642
+ if (!rootDirectoryUrl) {
6643
+ return [];
6644
+ }
6645
+ const now = Date.now();
6646
+ if (cache && now - cachedAt < SCAN_TTL_MS) {
6647
+ return cache;
6648
+ }
6649
+ const fileResultArray = await collectFiles({
6650
+ directoryUrl: rootDirectoryUrl,
6651
+ associations: HTML_PAGE_ASSOCIATIONS,
6652
+ predicate: (meta) => Boolean(meta.page),
6653
+ });
6654
+ const findPackageDirectory = createPackageDirectoryFinder(rootDirectoryUrl);
6655
+ const pages = fileResultArray.map(({ relativeUrl, meta }) => {
6656
+ const fileUrl = new URL(relativeUrl, rootDirectoryUrl).href;
6657
+ const packageDirectoryUrl = findPackageDirectory(
6658
+ new URL("./", fileUrl).href,
6659
+ );
6660
+ return {
6661
+ url: `/${relativeUrl}`,
6662
+ kind: readKind(meta),
6663
+ // Relative to the root and without its trailing slash, which is how a
6664
+ // tree names its own nodes.
6665
+ packageUrl: packageDirectoryUrl
6666
+ ? `/${packageDirectoryUrl.slice(String(rootDirectoryUrl).length).replace(/\/$/, "")}`
6667
+ : null,
6668
+ };
6669
+ });
6670
+ cache = pages;
6671
+ cachedAt = now;
6672
+ return pages;
6673
+ };
6674
+ };
6675
+
6530
6676
  /*
6531
6677
  * NICE TO HAVE:
6532
6678
  *
@@ -7171,6 +7317,8 @@ const jsenvPluginProtocolFile = ({
7171
7317
  packageDirectory,
7172
7318
  sourceFilesConfig,
7173
7319
  }) => {
7320
+ const listHtmlPages = createHtmlPageLister({ rootDirectoryUrl });
7321
+
7174
7322
  return [
7175
7323
  jsenvPluginFsRedirection({
7176
7324
  spa,
@@ -7226,6 +7374,27 @@ const jsenvPluginProtocolFile = ({
7226
7374
  );
7227
7375
  },
7228
7376
  },
7377
+ {
7378
+ name: "jsenv:html_pages",
7379
+ appliesDuring: "dev",
7380
+ serverRoutes: [
7381
+ {
7382
+ endpoint: "GET /.internal/pages.json",
7383
+ description:
7384
+ "The .html files served under the source directory, as urls to navigate to.",
7385
+ availableMediaTypes: ["application/json"],
7386
+ declarationSource: import.meta.url,
7387
+ fetch: async () => ({
7388
+ status: 200,
7389
+ headers: {
7390
+ "content-type": "application/json",
7391
+ "cache-control": "no-store",
7392
+ },
7393
+ body: JSON.stringify(await listHtmlPages()),
7394
+ }),
7395
+ },
7396
+ ],
7397
+ },
7229
7398
  ...(directoryListing
7230
7399
  ? [
7231
7400
  jsenvPluginDirectoryListing({
@@ -3625,6 +3625,96 @@ const readStat = (
3625
3625
  });
3626
3626
  };
3627
3627
 
3628
+ const collectFiles = async ({
3629
+ signal = new AbortController().signal,
3630
+ directoryUrl,
3631
+ associations,
3632
+ predicate,
3633
+ }) => {
3634
+ const rootDirectoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
3635
+ if (typeof predicate !== "function") {
3636
+ throw new TypeError(`predicate must be a function, got ${predicate}`);
3637
+ }
3638
+ associations = URL_META.resolveAssociations(associations, rootDirectoryUrl);
3639
+
3640
+ const collectOperation = Abort.startOperation();
3641
+ collectOperation.addAbortSignal(signal);
3642
+
3643
+ const matchingFileResultArray = [];
3644
+ const visitDirectory = async (directoryUrl) => {
3645
+ collectOperation.throwIfAborted();
3646
+ const directoryItems = await readDirectory(directoryUrl);
3647
+
3648
+ await Promise.all(
3649
+ directoryItems.map(async (directoryItem) => {
3650
+ const directoryChildNodeUrl = `${directoryUrl}${directoryItem}`;
3651
+ collectOperation.throwIfAborted();
3652
+ const directoryChildNodeStats = await readEntryStat(
3653
+ directoryChildNodeUrl,
3654
+ {
3655
+ // we ignore symlink because recursively traversed
3656
+ // so symlinked file will be discovered.
3657
+ // Moreover if they lead outside of directoryPath it can become a problem
3658
+ // like infinite recursion of whatever.
3659
+ // that we could handle using an object of pathname already seen but it will be useless
3660
+ // because directoryPath is recursively traversed
3661
+ followLink: false,
3662
+ },
3663
+ );
3664
+
3665
+ if (directoryChildNodeStats.isDirectory()) {
3666
+ const subDirectoryUrl = `${directoryChildNodeUrl}/`;
3667
+ if (
3668
+ !URL_META.urlChildMayMatch({
3669
+ url: subDirectoryUrl,
3670
+ associations,
3671
+ predicate,
3672
+ })
3673
+ ) {
3674
+ return;
3675
+ }
3676
+ await visitDirectory(subDirectoryUrl);
3677
+ return;
3678
+ }
3679
+
3680
+ if (directoryChildNodeStats.isFile()) {
3681
+ const meta = URL_META.applyAssociations({
3682
+ url: directoryChildNodeUrl,
3683
+ associations,
3684
+ });
3685
+ if (!predicate(meta)) return;
3686
+ const relativeUrl = urlToRelativeUrl(
3687
+ directoryChildNodeUrl,
3688
+ rootDirectoryUrl,
3689
+ );
3690
+ matchingFileResultArray.push({
3691
+ url: new URL(relativeUrl, rootDirectoryUrl).href,
3692
+ relativeUrl: decodeURIComponent(relativeUrl),
3693
+ meta,
3694
+ fileStats: directoryChildNodeStats,
3695
+ });
3696
+ return;
3697
+ }
3698
+ }),
3699
+ );
3700
+ };
3701
+
3702
+ try {
3703
+ await visitDirectory(rootDirectoryUrl);
3704
+
3705
+ // When we operate on thoose files later it feels more natural
3706
+ // to perform operation in the same order they appear in the filesystem.
3707
+ // It also allow to get a predictable return value.
3708
+ // For that reason we sort matchingFileResultArray
3709
+ matchingFileResultArray.sort((leftFile, rightFile) => {
3710
+ return comparePathnames(leftFile.relativeUrl, rightFile.relativeUrl);
3711
+ });
3712
+ return matchingFileResultArray;
3713
+ } finally {
3714
+ await collectOperation.end();
3715
+ }
3716
+ };
3717
+
3628
3718
  const writeEntryPermissionsSync = (source, permissions) => {
3629
3719
  const sourceUrl = assertAndNormalizeFileUrl(source);
3630
3720
 
@@ -11141,4 +11231,4 @@ const escapeRegexpSpecialChars = (string) => {
11141
11231
  });
11142
11232
  };
11143
11233
 
11144
- export { ANSI, Abort, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, UNICODE, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, bufferToEtag, clearDirectorySync, compareFileUrls, comparePathnames, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createDynamicLog, createLogger, createLookupPackageDirectory, createTaskLog, distributePercentages, ensureEmptyDirectory, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, escapeRegexpSpecialChars, generateContentFrame, getCallerPosition, getExtensionsToTry, humanizeDuration, humanizeFileSize, humanizeMemory, inferRuntimeCompatFromClosestPackage, injectQueryParamIntoSpecifierWithoutEncoding, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, moveUrl, nodeDefaultRuntimeCompat, normalizeImportMap, normalizeUrl, raceProcessTeardownEvents, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, renderBigSection, renderDetails, renderTable, renderUrlOrRelativeUrlFilename, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, startMonitoringCpuUsage, startMonitoringMemoryUsage, stringifyUrlSite, updateJsonFileSync, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFileSync };
11234
+ export { ANSI, Abort, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, UNICODE, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, bufferToEtag, clearDirectorySync, collectFiles, compareFileUrls, comparePathnames, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createDynamicLog, createLogger, createLookupPackageDirectory, createTaskLog, distributePercentages, ensureEmptyDirectory, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, escapeRegexpSpecialChars, generateContentFrame, getCallerPosition, getExtensionsToTry, humanizeDuration, humanizeFileSize, humanizeMemory, inferRuntimeCompatFromClosestPackage, injectQueryParamIntoSpecifierWithoutEncoding, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, moveUrl, nodeDefaultRuntimeCompat, normalizeImportMap, normalizeUrl, raceProcessTeardownEvents, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, renderBigSection, renderDetails, renderTable, renderUrlOrRelativeUrlFilename, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, startMonitoringCpuUsage, startMonitoringMemoryUsage, stringifyUrlSite, updateJsonFileSync, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFileSync };
@@ -303,8 +303,11 @@ button:hover {
303
303
  const v = version && version !== "unknown" ? ` ${version}` : "";
304
304
  return `${label}${v}`;
305
305
  };
306
+ // The ip is part of who a client is: it tells the main client
307
+ // (localhost — this machine) from a device on the network.
308
+ const ipLabel = client.local ? "localhost" : client.ip;
306
309
  return (
307
- [part(client.runtime, RUNTIME_LABELS), part(client.os)]
310
+ [part(client.runtime, RUNTIME_LABELS), part(client.os), ipLabel]
308
311
  .filter(Boolean)
309
312
  .join(" · ") || clientId.slice(0, 8)
310
313
  );
@@ -275,12 +275,12 @@ ul.list li:last-child {
275
275
  }
276
276
 
277
277
  const headHtml = `<tr>
278
- <th>First seen</th><th>OS</th><th>Browser</th><th>Active tab</th>
278
+ <th>First seen</th><th>OS</th><th>Browser</th><th>IP</th><th>Active tab</th>
279
279
  <th>Last activity</th><th>Logs</th><th></th>
280
280
  </tr>`;
281
281
  document.getElementById("head").innerHTML = headHtml;
282
282
  document.getElementById("head2").innerHTML = headHtml;
283
- const COLSPAN = 7;
283
+ const COLSPAN = 8;
284
284
 
285
285
  const ago = (ts) => {
286
286
  const s = Math.round((Date.now() - ts) / 1000);
@@ -420,11 +420,21 @@ ul.list li:last-child {
420
420
  <a class="monitor" href="/.internal/client?id=${encodeURIComponent(d.id)}">Monitor →</a>
421
421
  </div>`;
422
422
  };
423
+ // The ip says how the client reaches the server, which is what tells the
424
+ // main client (this machine, via localhost) from a device on the network.
425
+ const ipCell = (d) => {
426
+ if (!d.ip) {
427
+ return '<span class="muted">—</span>';
428
+ }
429
+ const label = d.local ? "localhost" : d.ip;
430
+ return `<span title="${escapeHtml(d.ip)}">${escapeHtml(label)}</span>`;
431
+ };
423
432
  const rowHtml = (d) => {
424
433
  return `<tr>
425
434
  <td>${firstSeenCell(d)}</td>
426
435
  <td>${named(d.os)}</td>
427
436
  <td>${named(d.runtime)}</td>
437
+ <td>${ipCell(d)}</td>
428
438
  <td>${tabCell(d)}</td>
429
439
  <td>${activityCell(d)}</td>
430
440
  <td>${d.logCount}</td>
@@ -537,9 +547,7 @@ ul.list li:last-child {
537
547
  return navigablePages;
538
548
  }
539
549
  try {
540
- navigablePages = await (
541
- await fetch("/.internal/clients/pages.json")
542
- ).json();
550
+ navigablePages = await (await fetch("/.internal/pages.json")).json();
543
551
  } catch {
544
552
  navigablePages = [];
545
553
  }