diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..f2a88ad --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +VITE_DEV_SERVER_HOST=172.16.4.220 +VITE_DEV_SERVER_PORT=4000 diff --git a/.pnp.cjs b/.pnp.cjs index 23f798f..005d335 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -40,6 +40,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-react-hooks", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:7.1.1"],\ ["eslint-plugin-react-refresh", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:0.5.2"],\ ["globals", "npm:17.6.0"],\ + ["jspdf", "npm:4.2.1"],\ ["network-tools", "workspace:."],\ ["react", "npm:19.2.6"],\ ["react-dom", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:19.2.6"],\ @@ -215,6 +216,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@babel/runtime", [\ + ["npm:7.29.7", {\ + "packageLocation": "../../.yarn/berry/cache/@babel-runtime-npm-7.29.7-423e8cd4b6-10c0.zip/node_modules/@babel/runtime/",\ + "packageDependencies": [\ + ["@babel/runtime", "npm:7.29.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@babel/template", [\ ["npm:7.28.6", {\ "packageLocation": "../../.yarn/berry/cache/@babel-template-npm-7.28.6-bff3bc3923-10c0.zip/node_modules/@babel/template/",\ @@ -769,6 +779,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/pako", [\ + ["npm:2.0.4", {\ + "packageLocation": "../../.yarn/berry/cache/@types-pako-npm-2.0.4-ac100d59c9-10c0.zip/node_modules/@types/pako/",\ + "packageDependencies": [\ + ["@types/pako", "npm:2.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@types/raf", [\ + ["npm:3.4.3", {\ + "packageLocation": "../../.yarn/berry/cache/@types-raf-npm-3.4.3-afd3c6d2bc-10c0.zip/node_modules/@types/raf/",\ + "packageDependencies": [\ + ["@types/raf", "npm:3.4.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/react", [\ ["npm:19.2.15", {\ "packageLocation": "../../.yarn/berry/cache/@types-react-npm-19.2.15-767accf7c6-10c0.zip/node_modules/@types/react/",\ @@ -799,6 +827,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/trusted-types", [\ + ["npm:2.0.7", {\ + "packageLocation": "../../.yarn/berry/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-10c0.zip/node_modules/@types/trusted-types/",\ + "packageDependencies": [\ + ["@types/trusted-types", "npm:2.0.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@vitejs/plugin-react", [\ ["npm:6.0.2", {\ "packageLocation": "../../.yarn/berry/cache/@vitejs-plugin-react-npm-6.0.2-0f2620b1d7-10c0.zip/node_modules/@vitejs/plugin-react/",\ @@ -952,6 +989,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["base64-arraybuffer", [\ + ["npm:1.0.2", {\ + "packageLocation": "../../.yarn/berry/cache/base64-arraybuffer-npm-1.0.2-18f4899892-10c0.zip/node_modules/base64-arraybuffer/",\ + "packageDependencies": [\ + ["base64-arraybuffer", "npm:1.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["baseline-browser-mapping", [\ ["npm:2.10.31", {\ "packageLocation": "../../.yarn/berry/cache/baseline-browser-mapping-npm-2.10.31-11b5c14e52-10c0.zip/node_modules/baseline-browser-mapping/",\ @@ -1005,6 +1051,23 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["canvg", [\ + ["npm:3.0.11", {\ + "packageLocation": "../../.yarn/berry/cache/canvg-npm-3.0.11-0944b07da2-10c0.zip/node_modules/canvg/",\ + "packageDependencies": [\ + ["@babel/runtime", "npm:7.29.7"],\ + ["@types/raf", "npm:3.4.3"],\ + ["canvg", "npm:3.0.11"],\ + ["core-js", "npm:3.49.0"],\ + ["raf", "npm:3.4.1"],\ + ["regenerator-runtime", "npm:0.13.11"],\ + ["rgbcolor", "npm:1.0.1"],\ + ["stackblur-canvas", "npm:2.7.0"],\ + ["svg-pathdata", "npm:6.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["chownr", [\ ["npm:3.0.0", {\ "packageLocation": "../../.yarn/berry/cache/chownr-npm-3.0.0-5275e85d25-10c0.zip/node_modules/chownr/",\ @@ -1033,6 +1096,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["core-js", [\ + ["npm:3.49.0", {\ + "packageLocation": "../../.yarn/berry/cache/core-js-npm-3.49.0-0a974f48cc-10c0.zip/node_modules/core-js/",\ + "packageDependencies": [\ + ["core-js", "npm:3.49.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["cross-spawn", [\ ["npm:7.0.6", {\ "packageLocation": "../../.yarn/berry/cache/cross-spawn-npm-7.0.6-264bddf921-10c0.zip/node_modules/cross-spawn/",\ @@ -1045,6 +1117,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["css-line-break", [\ + ["npm:2.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/css-line-break-npm-2.1.0-a1fd810d69-10c0.zip/node_modules/css-line-break/",\ + "packageDependencies": [\ + ["css-line-break", "npm:2.1.0"],\ + ["utrie", "npm:1.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["csstype", [\ ["npm:3.2.3", {\ "packageLocation": "../../.yarn/berry/cache/csstype-npm-3.2.3-741053244e-10c0.zip/node_modules/csstype/",\ @@ -1104,6 +1186,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["dompurify", [\ + ["npm:3.4.7", {\ + "packageLocation": "../../.yarn/berry/cache/dompurify-npm-3.4.7-440dacdb20-10c0.zip/node_modules/dompurify/",\ + "packageDependencies": [\ + ["@types/trusted-types", "npm:2.0.7"],\ + ["dompurify", "npm:3.4.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dunder-proto", [\ ["npm:1.0.1", {\ "packageLocation": "../../.yarn/berry/cache/dunder-proto-npm-1.0.1-90eb6829db-10c0.zip/node_modules/dunder-proto/",\ @@ -1432,6 +1524,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fast-png", [\ + ["npm:6.4.0", {\ + "packageLocation": "../../.yarn/berry/cache/fast-png-npm-6.4.0-7871b2b5a5-10c0.zip/node_modules/fast-png/",\ + "packageDependencies": [\ + ["@types/pako", "npm:2.0.4"],\ + ["fast-png", "npm:6.4.0"],\ + ["iobuffer", "npm:5.4.0"],\ + ["pako", "npm:2.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fdir", [\ ["npm:6.5.0", {\ "packageLocation": "../../.yarn/berry/cache/fdir-npm-6.5.0-8814a0dec7-10c0.zip/node_modules/fdir/",\ @@ -1454,6 +1558,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fflate", [\ + ["npm:0.8.3", {\ + "packageLocation": "../../.yarn/berry/cache/fflate-npm-0.8.3-35acaff861-10c0.zip/node_modules/fflate/",\ + "packageDependencies": [\ + ["fflate", "npm:0.8.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["file-entry-cache", [\ ["npm:8.0.0", {\ "packageLocation": "../../.yarn/berry/cache/file-entry-cache-npm-8.0.0-5b09d19a83-10c0.zip/node_modules/file-entry-cache/",\ @@ -1686,6 +1799,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["html2canvas", [\ + ["npm:1.4.1", {\ + "packageLocation": "../../.yarn/berry/cache/html2canvas-npm-1.4.1-fa13983145-10c0.zip/node_modules/html2canvas/",\ + "packageDependencies": [\ + ["css-line-break", "npm:2.1.0"],\ + ["html2canvas", "npm:1.4.1"],\ + ["text-segmentation", "npm:1.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["https-proxy-agent", [\ ["npm:5.0.1", {\ "packageLocation": "../../.yarn/berry/cache/https-proxy-agent-npm-5.0.1-42d65f358e-10c0.zip/node_modules/https-proxy-agent/",\ @@ -1715,6 +1839,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["iobuffer", [\ + ["npm:5.4.0", {\ + "packageLocation": "../../.yarn/berry/cache/iobuffer-npm-5.4.0-2cf4f91275-10c0.zip/node_modules/iobuffer/",\ + "packageDependencies": [\ + ["iobuffer", "npm:5.4.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["is-extglob", [\ ["npm:2.1.1", {\ "packageLocation": "../../.yarn/berry/cache/is-extglob-npm-2.1.1-0870ea68b5-10c0.zip/node_modules/is-extglob/",\ @@ -1804,6 +1937,22 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["jspdf", [\ + ["npm:4.2.1", {\ + "packageLocation": "../../.yarn/berry/cache/jspdf-npm-4.2.1-4a36558b77-10c0.zip/node_modules/jspdf/",\ + "packageDependencies": [\ + ["@babel/runtime", "npm:7.29.7"],\ + ["canvg", "npm:3.0.11"],\ + ["core-js", "npm:3.49.0"],\ + ["dompurify", "npm:3.4.7"],\ + ["fast-png", "npm:6.4.0"],\ + ["fflate", "npm:0.8.3"],\ + ["html2canvas", "npm:1.4.1"],\ + ["jspdf", "npm:4.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["keyv", [\ ["npm:4.5.4", {\ "packageLocation": "../../.yarn/berry/cache/keyv-npm-4.5.4-4c8e2cf7f7-10c0.zip/node_modules/keyv/",\ @@ -2065,6 +2214,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-react-hooks", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:7.1.1"],\ ["eslint-plugin-react-refresh", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:0.5.2"],\ ["globals", "npm:17.6.0"],\ + ["jspdf", "npm:4.2.1"],\ ["network-tools", "workspace:."],\ ["react", "npm:19.2.6"],\ ["react-dom", "virtual:1bef36b42a34c769545e238167cf96a97395f203a4be67468ec9e7c364af0538d9297399544e3b7eb227555c0667de3f416c95893ffd698b011448edf0cd18b9#npm:19.2.6"],\ @@ -2147,6 +2297,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["pako", [\ + ["npm:2.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/pako-npm-2.1.0-78df11948c-10c0.zip/node_modules/pako/",\ + "packageDependencies": [\ + ["pako", "npm:2.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["path-exists", [\ ["npm:4.0.0", {\ "packageLocation": "../../.yarn/berry/cache/path-exists-npm-4.0.0-e9e4f63eb0-10c0.zip/node_modules/path-exists/",\ @@ -2165,6 +2324,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["performance-now", [\ + ["npm:2.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/performance-now-npm-2.1.0-45e3ce7e49-10c0.zip/node_modules/performance-now/",\ + "packageDependencies": [\ + ["performance-now", "npm:2.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["picocolors", [\ ["npm:1.1.1", {\ "packageLocation": "../../.yarn/berry/cache/picocolors-npm-1.1.1-4fede47cf1-10c0.zip/node_modules/picocolors/",\ @@ -2231,6 +2399,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["raf", [\ + ["npm:3.4.1", {\ + "packageLocation": "../../.yarn/berry/cache/raf-npm-3.4.1-c25d48d76e-10c0.zip/node_modules/raf/",\ + "packageDependencies": [\ + ["performance-now", "npm:2.1.0"],\ + ["raf", "npm:3.4.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react", [\ ["npm:19.2.6", {\ "packageLocation": "../../.yarn/berry/cache/react-npm-19.2.6-f326db5d50-10c0.zip/node_modules/react/",\ @@ -2263,6 +2441,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["regenerator-runtime", [\ + ["npm:0.13.11", {\ + "packageLocation": "../../.yarn/berry/cache/regenerator-runtime-npm-0.13.11-90bf536060-10c0.zip/node_modules/regenerator-runtime/",\ + "packageDependencies": [\ + ["regenerator-runtime", "npm:0.13.11"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["rgbcolor", [\ + ["npm:1.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/rgbcolor-npm-1.0.1-5d8cee2258-10c0.zip/node_modules/rgbcolor/",\ + "packageDependencies": [\ + ["rgbcolor", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["rolldown", [\ ["npm:1.0.1", {\ "packageLocation": "../../.yarn/berry/cache/rolldown-npm-1.0.1-8adafc3459-10c0.zip/node_modules/rolldown/",\ @@ -2366,6 +2562,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["stackblur-canvas", [\ + ["npm:2.7.0", {\ + "packageLocation": "../../.yarn/berry/cache/stackblur-canvas-npm-2.7.0-69df5c1ce4-10c0.zip/node_modules/stackblur-canvas/",\ + "packageDependencies": [\ + ["stackblur-canvas", "npm:2.7.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["svg-pathdata", [\ + ["npm:6.0.3", {\ + "packageLocation": "../../.yarn/berry/cache/svg-pathdata-npm-6.0.3-b9a9612535-10c0.zip/node_modules/svg-pathdata/",\ + "packageDependencies": [\ + ["svg-pathdata", "npm:6.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tar", [\ ["npm:7.5.15", {\ "packageLocation": "../../.yarn/berry/cache/tar-npm-7.5.15-f915901510-10c0.zip/node_modules/tar/",\ @@ -2380,6 +2594,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["text-segmentation", [\ + ["npm:1.0.3", {\ + "packageLocation": "../../.yarn/berry/cache/text-segmentation-npm-1.0.3-634201c349-10c0.zip/node_modules/text-segmentation/",\ + "packageDependencies": [\ + ["text-segmentation", "npm:1.0.3"],\ + ["utrie", "npm:1.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tinyglobby", [\ ["npm:0.2.16", {\ "packageLocation": "../../.yarn/berry/cache/tinyglobby-npm-0.2.16-102914a73b-10c0.zip/node_modules/tinyglobby/",\ @@ -2453,6 +2677,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["utrie", [\ + ["npm:1.0.2", {\ + "packageLocation": "../../.yarn/berry/cache/utrie-npm-1.0.2-909cbd985c-10c0.zip/node_modules/utrie/",\ + "packageDependencies": [\ + ["base64-arraybuffer", "npm:1.0.2"],\ + ["utrie", "npm:1.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vite", [\ ["npm:8.0.13", {\ "packageLocation": "../../.yarn/berry/cache/vite-npm-8.0.13-f2ad0579c2-10c0.zip/node_modules/vite/",\ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 216f591..16d0991 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/package.json b/package.json index 6831e80..e9cf444 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.6.0", + "jspdf": "^4.2.1", "react": "^19.2.6", "react-dom": "^19.2.6", "socket.io-client": "^4.6.1" diff --git a/src/App.css b/src/App.css index 4eb40f9..7e73886 100644 --- a/src/App.css +++ b/src/App.css @@ -117,6 +117,36 @@ body { border-radius: 10px; } +.results-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.btn-export { + background: #fff; + color: var(--accent); + border: 1.5px solid var(--accent); + border-radius: var(--radius); + padding: 6px 14px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s; +} +.btn-export:hover:not(:disabled) { + background: var(--accent); + color: #fff; +} +.btn-export:disabled { + opacity: 0.4; + cursor: not-allowed; +} + /* ── URL input ──────────────────────────────────────────────────────────────── */ .host-editor { display: flex; flex-direction: column; gap: 8px; } @@ -139,6 +169,70 @@ body { } .url-input::placeholder { color: var(--muted); } +/* ── Checks ─────────────────────────────────────────────────────────────────── */ +.checks-editor { + display: flex; + flex-direction: column; + gap: 8px; +} + +.checks-toolbar { + display: flex; + gap: 8px; +} + +.checks-action { + background: #fff; + border: 1px solid var(--border2); + border-radius: 4px; + padding: 2px 8px; + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted); + cursor: pointer; +} +.checks-action:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} +.checks-action:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.checks-list { + display: flex; + flex-direction: column; + gap: 6px; + background: #fff; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 10px 12px; + box-shadow: var(--shadow); +} + +.check-item { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text); + cursor: pointer; + user-select: none; +} + +.check-item input { + accent-color: var(--accent); + width: 14px; + height: 14px; + cursor: pointer; +} + +.check-item input:disabled { + cursor: not-allowed; +} + /* ── Run button ─────────────────────────────────────────────────────────────── */ .btn-run { background: var(--accent); @@ -380,11 +474,23 @@ pre { display: flex; align-items: center; gap: 8px; + min-width: 0; +} + +.server-group-names { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; } .server-host-sub { - font-size: 0.75rem; - opacity: 0.6; + font-family: var(--font-mono); + font-size: 10px; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .server-status-dot { @@ -398,6 +504,68 @@ pre { .group-ok { border-color: #22c55e44; } .group-err { border-color: #ef444444; } +.group-unavailable { border-color: #ef4444; border-left: 3px solid var(--err); } + +.server-unavailable-badge { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--err); + background: #fdeaee; + padding: 2px 8px; + border-radius: 4px; +} + +.card-unavailable { + border-left: 3px solid var(--err); +} + +.upstream-unavailable-block { + padding: 8px 12px; +} + +.upstream-unavailable { + font-family: var(--font-mono); + display: flex; + flex-direction: column; + gap: 2px; +} + +.upstream-head { + display: flex; + align-items: baseline; + gap: 8px; +} + +.upstream-head strong { + font-size: 12px; + color: var(--text); +} + +.upstream-status { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.4px; + font-weight: 600; + color: var(--err); +} + +.upstream-line { + font-size: 10px; + line-height: 1.3; + color: var(--muted); + word-break: break-word; +} + +.upstream-meta { + color: #6a7490; +} + +.upstream-error { + color: var(--err); +} .server-group-results { padding: 8px; diff --git a/src/App.jsx b/src/App.jsx index 06fab20..80117a9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,29 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { io } from "socket.io-client"; import axios from "axios"; +import { jsPDF } from "jspdf"; import "./App.css"; const BASE = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, ""); -const API_BASE = `${BASE}/api`; -const SOCKET_IO_PATH = `${BASE}/socket.io`; +const IS_DEV = import.meta.env.DEV; +const DEV_HOST = import.meta.env.VITE_DEV_SERVER_HOST ?? "172.16.4.220"; +const DEV_PORT = import.meta.env.VITE_DEV_SERVER_PORT ?? "4000"; +const DEV_SERVER_URL = `http://${DEV_HOST}:${DEV_PORT}`; + +const API_BASE = IS_DEV ? `${DEV_SERVER_URL}/api` : `${BASE}/api`; +const SOCKET_URL = IS_DEV ? DEV_SERVER_URL : undefined; +const SOCKET_IO_PATH = IS_DEV ? "/socket.io" : `${BASE}/socket.io`; const DEFAULT_HOSTS = [{ url: "", method: "GET" }]; +const CHECK_OPTIONS = [ + { id: "ping", label: "Ping" }, + { id: "curl", label: "Curl" }, + { id: "traceroute", label: "Traceroute" }, +]; + +const DEFAULT_CHECKS = CHECK_OPTIONS.map((c) => c.id); + function getToolBody(result) { if (!result) return null; const d = result.data; @@ -40,6 +55,122 @@ function isResultError(result) { return false; } +function isUpstreamUnavailable(result) { + if (!result || result.type !== "error") return false; + if (result.code === "ECONNREFUSED") return true; + return /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EHOSTUNREACH/i.test( + result.error || "", + ); +} + +function isToolError(result) { + return ( + result?.type === "error" && + (result.endpoint || result.serverName || result.host) + ); +} + +function appendResult(prev, entry, index) { + if (typeof index === "number") { + const next = prev.length ? [...prev] : []; + while (next.length <= index) next.push(null); + next[index] = entry; + return next; + } + if (!prev.length) return [entry]; + const emptyIdx = prev.findIndex((r) => r == null); + if (emptyIdx >= 0) { + const next = [...prev]; + next[emptyIdx] = entry; + return next; + } + return [...prev, entry]; +} + +function getResultOutput(result) { + if (!result) return ""; + if (result.type === "error") return result.error || "Error"; + const body = getToolBody(result); + if (!body) return ""; + const parts = [body.stdout, body.trace, body.body].filter(Boolean); + if (parts.length) { + const text = parts.join("\n"); + return body.stderr ? `${text}\n\nstderr:\n${body.stderr}` : text; + } + if (body.stderr) return body.stderr; + return JSON.stringify(body, null, 2); +} + +function exportResultsToPdf(groupedResults, targetUrl) { + const doc = new jsPDF({ unit: "pt", format: "a4" }); + const margin = 40; + const maxWidth = 515; + const pageHeight = doc.internal.pageSize.getHeight(); + const lineHeight = 12; + let y = margin; + + const ensureSpace = (needed = lineHeight) => { + if (y + needed > pageHeight - margin) { + doc.addPage(); + y = margin; + } + }; + + const addLines = (text, { bold = false, size = 10, gap = 0 } = {}) => { + doc.setFont("helvetica", bold ? "bold" : "normal"); + doc.setFontSize(size); + const lines = doc.splitTextToSize(String(text), maxWidth); + for (const line of lines) { + ensureSpace(); + doc.text(line, margin, y); + y += lineHeight; + } + y += gap; + }; + + addLines("Network tools — diagnostic report", { bold: true, size: 14, gap: 4 }); + addLines(`Generated: ${new Date().toLocaleString()}`, { size: 9, gap: 2 }); + if (targetUrl) addLines(`Target URL: ${targetUrl}`, { size: 9, gap: 10 }); + + for (const { serverName, items, errors } of groupedResults) { + addLines(`${serverName} — ${errors} error(s) / ${items.length} total`, { + bold: true, + size: 11, + gap: 6, + }); + + for (const { result } of items) { + const failed = isResultError(result); + const status = + getToolBody(result)?.returncode ?? result.status ?? (failed ? "FAIL" : "OK"); + const header = [ + result.endpoint || "request", + result.elapsed != null ? `${result.elapsed}ms` : null, + `exit ${status}`, + ] + .filter(Boolean) + .join(" · "); + + addLines(header, { bold: true, size: 10, gap: 4 }); + + doc.setFont("courier", "normal"); + doc.setFontSize(9); + const output = getResultOutput(result) || "(no output)"; + const lines = doc.splitTextToSize(output, maxWidth); + for (const line of lines) { + ensureSpace(); + doc.text(line, margin, y); + y += lineHeight; + } + y += 8; + } + y += 4; + } + + const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-"); + doc.save(`diagnostic-report-${stamp}.pdf`); +} + // ─── StatusBadge ────────────────────────────────────────────────────────────── function StatusBadge({ status, ok }) { if (status === undefined) return PENDING; @@ -51,6 +182,66 @@ function StatusBadge({ status, ok }) { } // ─── ResultCard ─────────────────────────────────────────────────────────────── +function UpstreamUnavailableBlock({ + serverName, + host, + metadata = {}, + error, + code, +}) { + const meta = [ + metadata.location, + metadata.provider, + metadata.country, + metadata.tags?.length && metadata.tags.join(", "), + ] + .filter(Boolean) + .join(" · "); + + return ( +
+
+
+ {serverName} + unavailable +
+ {host &&
{host}
} + {meta &&
{meta}
} + {(code || error) && ( +
+ {[code, error].filter(Boolean).join(": ")} +
+ )} +
+
+ ); +} + +function ToolErrorCard({ result, index }) { + return ( +
+
+ #{index + 1} + + {result.endpoint || "request"} + +
+ {result.elapsed != null && ( + {result.elapsed}ms + )} + ERROR +
+
+
+
+ Request failed +

{result.error}

+
+
+
+ ); +} + function ToolResultCard({ result, index }) { const body = getToolBody(result); const processOk = body.returncode === 0; @@ -104,6 +295,10 @@ function ResultCard({ result, index }) { const isError = result?.type === "error"; const isPending = !result; + if (isToolError(result) && !isUpstreamUnavailable(result)) { + return ; + } + if (isToolResult(result)) { return ; } @@ -173,6 +368,53 @@ function HostEditor({ hosts, setHosts }) { ); } +function ChecksEditor({ checks, setChecks, disabled }) { + const toggle = (id) => { + setChecks((prev) => + prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id], + ); + }; + + const selectAll = () => setChecks(DEFAULT_CHECKS); + const clearAll = () => setChecks([]); + + return ( +
+
+ + +
+
+ {CHECK_OPTIONS.map(({ id, label }) => ( + + ))} +
+
+ ); +} + // ─── App ────────────────────────────────────────────────────────────────────── export default function App() { const socketRef = useRef(null); @@ -180,6 +422,7 @@ export default function App() { const [connected, setConnected] = useState(false); const [hosts, setHosts] = useState(DEFAULT_HOSTS); + const [checks, setChecks] = useState(DEFAULT_CHECKS); const [results, setResults] = useState([]); const [progress, setProgress] = useState(null); // { done, total } const [running, setRunning] = useState(false); @@ -192,7 +435,7 @@ export default function App() { // ── Connect socket once ────────────────────────────────────────────────── useEffect(() => { - const socket = io({ path: SOCKET_IO_PATH }); + const socket = io(SOCKET_URL, { path: SOCKET_IO_PATH }); socketRef.current = socket; socket.on("connect", () => { @@ -227,22 +470,7 @@ export default function App() { socket.on("tool:response", (payload) => { console.log("[socket] tool:response", payload); const entry = { ...payload, type: "tool" }; - setResults((prev) => { - if (typeof payload.index === "number") { - const next = prev.length ? [...prev] : []; - while (next.length <= payload.index) next.push(null); - next[payload.index] = entry; - return next; - } - if (!prev.length) return [entry]; - const emptyIdx = prev.findIndex((r) => r == null); - if (emptyIdx >= 0) { - const next = [...prev]; - next[emptyIdx] = entry; - return next; - } - return [...prev, entry]; - }); + setResults((prev) => appendResult(prev, entry, payload.index)); const idx = typeof payload.index === "number" ? payload.index : null; setProgress((p) => p && { ...p, done: Math.min((p.done ?? 0) + 1, p.total) }); const body = getToolBody(payload); @@ -254,13 +482,14 @@ export default function App() { socket.on("tool:error", (payload) => { console.log("[socket] tool:error", payload); - setResults((prev) => { - const next = [...prev]; - next[payload.index] = { ...payload, type: "error" }; - return next; - }); - setProgress((p) => p && { ...p, done: p.done + 1 }); - pushLog(`#${payload.index + 1} ${payload.url} → ERROR: ${payload.error}`); + const entry = { ...payload, type: "error" }; + setResults((prev) => appendResult(prev, entry, payload.index)); + const idx = typeof payload.index === "number" ? payload.index : null; + setProgress((p) => p && { ...p, done: Math.min((p.done ?? 0) + 1, p.total) }); + const label = payload.serverName || payload.host || payload.endpoint || "?"; + pushLog( + `${idx != null ? `#${idx + 1} ` : ""}${label} → ERROR: ${payload.error}`, + ); }); socket.on("batch:done", (data) => { @@ -278,13 +507,13 @@ export default function App() { if (!socketId || running) return; const url = hosts[0]?.url?.trim(); - if (!url) return; + if (!url || checks.length === 0) return; setRunning(true); setResults([]); setProgress(null); - const payload = { socketId, url }; + const payload = { socketId, url, checks }; console.log("sending:", JSON.stringify(payload)); try { @@ -308,8 +537,13 @@ export default function App() { }); return Array.from(groups.entries()).map(([serverName, data]) => ({ serverName, + host: data.items[0]?.result?.host || "", + metadata: data.items[0]?.result?.metadata || {}, items: data.items, errors: data.errors, + unavailable: data.items.filter(({ result }) => + isUpstreamUnavailable(result), + ).length, })); }, [results]); @@ -332,10 +566,17 @@ export default function App() {

задаём URL

+

проверки

+ + + {results.length === 0 && !running && (
@@ -385,12 +638,33 @@ export default function App() {
{groupedResults.map((group) => { - const { serverName, items, errors } = group; + const { serverName, host, metadata, items, errors, unavailable } = + group; + const allUnavailable = + unavailable > 0 && unavailable === items.length; + + if (allUnavailable) { + const sample = items.find(({ result: r }) => + isUpstreamUnavailable(r), + )?.result; + return ( + + ); + } + const isCollapsed = collapsedGroups.has(serverName); const hasErrors = errors > 0; const groupClass = `server-group ${ hasErrors ? "group-err" : "group-ok" }`; + return (
{isCollapsed ? "▶" : "▼"} - {serverName} +
+ {serverName} + {host && ( + {host} + )} + {metadata?.location && ( + + {metadata.location} + {metadata.provider ? ` · ${metadata.provider}` : ""} + + )} +
@@ -420,7 +705,11 @@ export default function App() { {!isCollapsed && (
{items.map(({ result, index }) => ( - + ))}
)} diff --git a/vite.config.js b/vite.config.js index 8e3c110..c730b58 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,4 +9,7 @@ export default defineConfig({ react(), babel({ presets: [reactCompilerPreset()] }) ], + server: { + host: true, + }, }) diff --git a/yarn.lock b/yarn.lock index e29d146..57d67a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,6 +144,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.28.6": + version: 7.29.7 + resolution: "@babel/runtime@npm:7.29.7" + checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e + languageName: node + linkType: hard + "@babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" @@ -572,6 +579,20 @@ __metadata: languageName: node linkType: hard +"@types/pako@npm:^2.0.3": + version: 2.0.4 + resolution: "@types/pako@npm:2.0.4" + checksum: 10c0/5765bf8bc7e77ee141c454118f03e544b8f6cb51eb257d82dc5830feeab8cd00818af3a1eabefdfbe8dd3ae9916ed5403937bf1031a0ee51deea27fdf4dccdfb + languageName: node + linkType: hard + +"@types/raf@npm:^3.4.0": + version: 3.4.3 + resolution: "@types/raf@npm:3.4.3" + checksum: 10c0/dea835f0daa399c51db9137f5337dc08a2b4a5f61f645658966ecabaebbbd0fd59551f384a1141e14e22a1cc5a591da7d4d88c60a525ad1399108b6dd2641d75 + languageName: node + linkType: hard + "@types/react-dom@npm:^19.2.3": version: 19.2.3 resolution: "@types/react-dom@npm:19.2.3" @@ -590,6 +611,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:^6.0.1": version: 6.0.2 resolution: "@vitejs/plugin-react@npm:6.0.2" @@ -703,6 +731,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.2": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 10c0/3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f + languageName: node + linkType: hard + "baseline-browser-mapping@npm:^2.10.12": version: 2.10.31 resolution: "baseline-browser-mapping@npm:2.10.31" @@ -753,6 +788,22 @@ __metadata: languageName: node linkType: hard +"canvg@npm:^3.0.11": + version: 3.0.11 + resolution: "canvg@npm:3.0.11" + dependencies: + "@babel/runtime": "npm:^7.12.5" + "@types/raf": "npm:^3.4.0" + core-js: "npm:^3.8.3" + raf: "npm:^3.4.1" + regenerator-runtime: "npm:^0.13.7" + rgbcolor: "npm:^1.0.1" + stackblur-canvas: "npm:^2.0.0" + svg-pathdata: "npm:^6.0.3" + checksum: 10c0/03ea5b69e97a4a476ef5fc8455d0da2718bd68cd8ace3e42718176bf4b7dfd80eb1d0eba47dcd04b34ed9a0c5c8ed849ead0f364dad0e35369254a45d5e098c4 + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -776,6 +827,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.6.0, core-js@npm:^3.8.3": + version: 3.49.0 + resolution: "core-js@npm:3.49.0" + checksum: 10c0/2e42edb47eda38fd5368380131623c8aa5d4a6b42164125b17744bdc08fa5ebbbdd06b4b4aa6ca3663470a560b0f2fba48e18f142dfe264b0039df85bc625694 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -787,6 +845,15 @@ __metadata: languageName: node linkType: hard +"css-line-break@npm:^2.1.0": + version: 2.1.0 + resolution: "css-line-break@npm:2.1.0" + dependencies: + utrie: "npm:^1.0.2" + checksum: 10c0/b2222d99d5daf7861ecddc050244fdce296fad74b000dcff6bdfb1eb16dc2ef0b9ffe2c1c965e3239bd05ebe9eadb6d5438a91592fa8648d27a338e827cf9048 + languageName: node + linkType: hard + "csstype@npm:^3.2.2": version: 3.2.3 resolution: "csstype@npm:3.2.3" @@ -827,6 +894,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.3.1": + version: 3.4.7 + resolution: "dompurify@npm:3.4.7" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/b39940e95259bfaea658cb2bec819d47f271f9fe47e4e3cfff1fb7876089264d7c21e36ea09dad0d9a0f70add502ce6787ba9e7467f2c7b996382eb1d8daa375 + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -1087,6 +1166,17 @@ __metadata: languageName: node linkType: hard +"fast-png@npm:^6.2.0": + version: 6.4.0 + resolution: "fast-png@npm:6.4.0" + dependencies: + "@types/pako": "npm:^2.0.3" + iobuffer: "npm:^5.3.2" + pako: "npm:^2.1.0" + checksum: 10c0/bca27a09d56a5ead536b11c1ddccf4fe44c7c0af88a8faefab6a397527d9c71b00c09b2055d0108807c438c50719dd2acbd7217ef88785b500888db26fa0358a + languageName: node + linkType: hard + "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -1099,6 +1189,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.1": + version: 0.8.3 + resolution: "fflate@npm:0.8.3" + checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7 + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -1300,6 +1397,16 @@ __metadata: languageName: node linkType: hard +"html2canvas@npm:^1.0.0-rc.5": + version: 1.4.1 + resolution: "html2canvas@npm:1.4.1" + dependencies: + css-line-break: "npm:^2.1.0" + text-segmentation: "npm:^1.0.3" + checksum: 10c0/6de86f75762b00948edf2ea559f16da0a1ec3facc4a8a7d3f35fcec59bb0c5970463478988ae3d9082152e0173690d46ebf4082e7ac803dd4817bae1d355c0db + languageName: node + linkType: hard + "https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -1324,6 +1431,13 @@ __metadata: languageName: node linkType: hard +"iobuffer@npm:^5.3.2": + version: 5.4.0 + resolution: "iobuffer@npm:5.4.0" + checksum: 10c0/1b3f9a5ea158bc63f038b374b42832acdb9db13ea9cfa4ed78723f30894986442f6c033dff4ae2fdf34724bf0fe432e3b86c11ca82049568c58b8d7fb47a6ac2 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -1400,6 +1514,30 @@ __metadata: languageName: node linkType: hard +"jspdf@npm:^4.2.1": + version: 4.2.1 + resolution: "jspdf@npm:4.2.1" + dependencies: + "@babel/runtime": "npm:^7.28.6" + canvg: "npm:^3.0.11" + core-js: "npm:^3.6.0" + dompurify: "npm:^3.3.1" + fast-png: "npm:^6.2.0" + fflate: "npm:^0.8.1" + html2canvas: "npm:^1.0.0-rc.5" + dependenciesMeta: + canvg: + optional: true + core-js: + optional: true + dompurify: + optional: true + html2canvas: + optional: true + checksum: 10c0/d905809b374de1060ab39ba4b1603935fcced314bef8f63f7fd272d3f96eff0b50a96e48d3c2063ac8e51b84a1442464ff2b037af13f54b082e456416da470b7 + languageName: node + linkType: hard + "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -1644,6 +1782,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^7.1.1" eslint-plugin-react-refresh: "npm:^0.5.2" globals: "npm:^17.6.0" + jspdf: "npm:^4.2.1" react: "npm:^19.2.6" react-dom: "npm:^19.2.6" socket.io-client: "npm:^4.6.1" @@ -1721,6 +1860,13 @@ __metadata: languageName: node linkType: hard +"pako@npm:^2.1.0": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 10c0/8e8646581410654b50eb22a5dfd71159cae98145bd5086c9a7a816ec0370b5f72b4648d08674624b3870a521e6a3daffd6c2f7bc00fdefc7063c9d8232ff5116 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -1735,6 +1881,13 @@ __metadata: languageName: node linkType: hard +"performance-now@npm:^2.1.0": + version: 2.1.0 + resolution: "performance-now@npm:2.1.0" + checksum: 10c0/22c54de06f269e29f640e0e075207af57de5052a3d15e360c09b9a8663f393f6f45902006c1e71aa8a5a1cdfb1a47fe268826f8496d6425c362f00f5bc3e85d9 + languageName: node + linkType: hard + "picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -1788,6 +1941,15 @@ __metadata: languageName: node linkType: hard +"raf@npm:^3.4.1": + version: 3.4.1 + resolution: "raf@npm:3.4.1" + dependencies: + performance-now: "npm:^2.1.0" + checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e + languageName: node + linkType: hard + "react-dom@npm:^19.2.6": version: 19.2.6 resolution: "react-dom@npm:19.2.6" @@ -1806,6 +1968,20 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.13.7": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 10c0/12b069dc774001fbb0014f6a28f11c09ebfe3c0d984d88c9bced77fdb6fedbacbca434d24da9ae9371bfbf23f754869307fb51a4c98a8b8b18e5ef748677ca24 + languageName: node + linkType: hard + +"rgbcolor@npm:^1.0.1": + version: 1.0.1 + resolution: "rgbcolor@npm:1.0.1" + checksum: 10c0/13af06c523351bac2854b85a22d1dfafd9310efd898e9bd96c8706f9aa09a3ddc8392ab00ae03d12950782164a97677f21834ffd84ffebf76ae106add319f956 + languageName: node + linkType: hard + "rolldown@npm:1.0.1": version: 1.0.1 resolution: "rolldown@npm:1.0.1" @@ -1934,6 +2110,20 @@ __metadata: languageName: node linkType: hard +"stackblur-canvas@npm:^2.0.0": + version: 2.7.0 + resolution: "stackblur-canvas@npm:2.7.0" + checksum: 10c0/df290d0629056d5bb43d37548d0b24cb8593c79d742650e68489abf61013db578c9980724c2508bb738d107204f2e2494ab94c3cf69d6b725caa9c63b8c7e272 + languageName: node + linkType: hard + +"svg-pathdata@npm:^6.0.3": + version: 6.0.3 + resolution: "svg-pathdata@npm:6.0.3" + checksum: 10c0/1ba4ad2fa81e86df37d6e78d3be9e664bbedf97773b725a863a85db384285be32dc37d9c0d61e477d89594ee95b967d2c53d6bee2d76420aab670ab4124a38b9 + languageName: node + linkType: hard + "tar@npm:^7.5.4": version: 7.5.15 resolution: "tar@npm:7.5.15" @@ -1947,6 +2137,15 @@ __metadata: languageName: node linkType: hard +"text-segmentation@npm:^1.0.3": + version: 1.0.3 + resolution: "text-segmentation@npm:1.0.3" + dependencies: + utrie: "npm:^1.0.2" + checksum: 10c0/8b9ae8524e3a332371060d0ca62f10ad49a13e954719ea689a6c3a8b8c15c8a56365ede2bb91c322fb0d44b6533785f0da603e066b7554d052999967fb72d600 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.16": version: 0.2.16 resolution: "tinyglobby@npm:0.2.16" @@ -2003,6 +2202,15 @@ __metadata: languageName: node linkType: hard +"utrie@npm:^1.0.2": + version: 1.0.2 + resolution: "utrie@npm:1.0.2" + dependencies: + base64-arraybuffer: "npm:^1.0.2" + checksum: 10c0/eaffe645bd81a39e4bc3abb23df5895e9961dbdd49748ef3b173529e8b06ce9dd1163e9705d5309a1c61ee41ffcb825e2043bc0fd1659845ffbdf4b1515dfdb4 + languageName: node + linkType: hard + "vite@npm:^8.0.12": version: 8.0.13 resolution: "vite@npm:8.0.13"