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 ( +
{result.error}
+