Refactor(layout): split backend-only code out of common/, group frontend utils

common/ had become the backend's library: 30 of its 41 modules were
imported by nothing in the frontend, so the name no longer said what
belonged there. It now holds only what the frontend or the build imports
too; backend-only code moved to server/, flat except for datasets/ (the
dataset engine and its readers). The *-db/ dataset directories stay in
common/: self-hosting docs and Docker volumes mount those paths.

frontend/utils/ keeps its app-wide modules at the root and gains ip/
(IP / ASN shaping and validation), report/ (the report pipeline) and
features/ (the pure layer of a single feature). Paths only — no logic
changes. The Dockerfile copies server/, and the AGENTS.md files say where
a new module goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jason5ng32andClaude Opus 5.5 committed 2026-10-05 13:10:02 +08:00
1 parent ec4d536b60
commit c29e48687b
209 files changed
+420 -405

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ localtest
.VSCodeCounter
# Offline datasets (common/datasets.js): every common/<name>-db/ directory
# Offline datasets (server/datasets/datasets.js): every common/<name>-db/ directory
# holds only downloaded data, its state and lock — created at runtime, never
# committed (CAIDA's data is under a non-commercial license).
common/*-db/
+7 -4
View File
@@ -33,7 +33,7 @@ Express 5 back-end API.
| Build | Vite + `@vitejs/plugin-vue`; Tailwind CSS v4 + `tw-animate-css` |
| UI | shadcn-vue copy-in primitives (reka-ui) · lucide icons · circle-flags via `@iconify/vue` · vaul-vue drawer · vue-sonner toast |
| Backend | Express 5 |
| Logger | `pino` singleton at `common/logger.js` (+ `pino-http`, opt-in) |
| Logger | `pino` singleton at `server/logger.js` (+ `pino-http`, opt-in) |
| Auth | Firebase Auth (optional, env-gated) |
| Error monitoring | Sentry — optional & env-gated on both halves: `@sentry/vue` (no `VITE_SENTRY_DSN_FRONTEND`, no Sentry in the build — see frontend/AGENTS.md) + `@sentry/node` (no `SENTRY_DSN_BACKEND`, never loaded — see api/AGENTS.md) |
| PWA | `manifest.webmanifest` only — installable but online-only, no service worker |
@@ -62,8 +62,11 @@ use npm / yarn — they'd produce a competing lockfile.
├── AGENTS.md / CLAUDE.md ← this file + Claude pointer to it
├── frontend/ ← Vue 3 SPA (see frontend/AGENTS.md)
├── api/ ← Express handlers (see api/AGENTS.md)
├── common/ ← code shared by both halves (valid-ip /
│ fetch-with-timeout / guards / logger / …)
├── server/ ← backend-only code (logger / guards / …;
│ datasets/ = the offline dataset engine)
├── common/ ← code the frontend or build imports too
│ (valid-ip / fetch-with-timeout / …) + the
│ dataset dirs (`*-db/`, a self-host mount point)
├── tests/ ← Node test runner specs
├── backend-server.js ← Express app (default port 11966)
├── sentry-instrument.js ← backend Sentry bootstrap via `node --import`;
@@ -112,7 +115,7 @@ use npm / yarn — they'd produce a competing lockfile.
### Logging (backend)
- **Always the shared logger** (`common/logger.js`) in backend files; bare
- **Always the shared logger** (`server/logger.js`) in backend files; bare
`console.*` is banned there (frontend keeps using `console.*`).
- Pino first-arg-is-context: `logger.error({ err, ip }, 'short message')`.
- Env knobs: `LOG_LEVEL` (default info), `LOG_FORMAT=json` for shippers,
+4 -3
View File
@@ -36,7 +36,8 @@ from building something that can't be merged.
```
frontend/ Vue 3 SPA (Pinia, vue-router, vue-i18n, Tailwind v4 + shadcn-vue)
api/ Express 5 handlers, one file per route (wired in backend-server.js)
common/ Code shared by both halves (validators, fetch helper, logger, …)
server/ Backend-only code (logger, guards, upstream clients, dataset engine, …)
common/ Code both halves import (validators, fetch helper, report schema, …)
tests/ Node test runner specs (node --test)
```
@@ -60,8 +61,8 @@ Details and rationale live in the AGENTS.md files; the headlines:
- **The `full` locales land together** — user-visible copy ships in `en` / `zh` / `zh-TW` /
`fr` / `ru` in the same PR (`frontend/locales/`; tests enforce it). `beta` locales are
exempt — they fall back to English.
- **Backend logging goes through the shared pino logger** (`common/logger.js`) —
no `console.*` in `api/` or `common/`.
- **Backend logging goes through the shared pino logger** (`server/logger.js`) —
no `console.*` in `api/`, `server/` or `common/`.
## Good places to start
+1
View File
@@ -29,6 +29,7 @@ COPY --from=build-stage /app/frontend-server.js ./
# npm start boots the backend with `--import ./sentry-instrument.js`
COPY --from=build-stage /app/sentry-instrument.js ./
COPY --from=build-stage /app/api ./api
COPY --from=build-stage /app/server ./server
COPY --from=build-stage /app/common ./common
EXPOSE 18966
+2 -2
View File
@@ -18,8 +18,8 @@ Helpful things to include:
- What an attacker can do with it, and what they'd need to start (a session? just a URL?).
- Steps to reproduce — a request, a payload, or a short script.
- Where the problem lives: front-end (`frontend/`), API handler (`api/`), or shared
code (`common/`).
- Where the problem lives: front-end (`frontend/`), API handler (`api/`), backend
code (`server/`), or shared code (`common/`).
- Whether you hit it on [ipcheck.ing](https://ipcheck.ing) or your own deployment, and
the version if self-hosted.
+28 -22
View File
@@ -1,15 +1,21 @@
# api/AGENTS.md
Conventions for Express 5 handlers under `api/` and shared back-end code
under `common/`. Universal rules live in ../AGENTS.md.
Conventions for Express 5 handlers under `api/` and the back-end code under
`server/` and `common/`. Universal rules live in ../AGENTS.md.
## Overview
The Express app lives in `backend-server.js` at the repo root — every route
is wired there and delegated to one handler module under `api/`. `common/`
holds shared back-end code (guards, logger, fetch helper, MaxMind / CAIDA
services, service-status poller), parts of which the frontend also imports
(`valid-ip.js`, `fetch-with-timeout.js`).
is wired there and delegated to one handler module under `api/`. Code the
handlers share is split by who imports it:
- **`server/`** — backend-only (guards, logger, upstream clients, service-status
poller, …), flat; `server/datasets/` is the offline dataset engine and its
readers (see "Offline datasets").
- **`common/`** — only what the frontend or the build imports too
(`valid-ip.js`, `fetch-with-timeout.js`, `report-schema.js`, …); it never
imports from `server/`. It also holds the dataset directories (`*-db/`),
which self-hosting docs and Docker volumes mount — they stay put.
Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` /
`ipapi-com` / `ipapi-is` / `ip2location-io` / `ip-sb` / `ipcheck-ing` /
@@ -23,7 +29,7 @@ and purpose — read those for specifics.
The exception to one-file-per-route is Cloudflare Radar: all Radar data rides
`/api/cfradar`, dispatched by `?view=` over `RADAR_VIEWS` in
`common/cf-radar.js`, where each view declares its guards, edge-cache TTL and
`server/cf-radar.js`, where each view declares its guards, edge-cache TTL and
fetch function — new Radar data is a view function plus a registry row, never
a new route. `api/data/` holds contributor-editable static config consumed by
handlers — currently `dns-resolvers.js`, the country-annotated resolver list
@@ -31,20 +37,20 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
### ASN data
- **`/api/asn-profile`** (`common/asn-profile.js`) runs each ASN source's core
- **`/api/asn-profile`** (`server/asn-profile.js`) runs each ASN source's core
under a deadline above the upstream timeouts it wraps (`INNER_TIMEOUTS`); a
failed or late one is just absent. Always 200 `{ asn, status, incomplete,
…sections }` (`ok` / `empty` / `error` / `disabled`; an upstream fault is
`error`, never `empty`; `incomplete` = `ok` but partial); 502 if all fail.
- **Relationship counts follow the graph:** `asn-connectivity` neighbour
lists come from local CAIDA (`common/as-relationships.js`), dropping the
lists come from local CAIDA (`server/as-relationships.js`), dropping the
providers the graph walk distrusts. The Radar `asn` view (ASN Info) carries
no prefix or relationship counts — the ASN Profile has both.
- **`/api/whois` `?q=`:** IPs (RDAP, whoiser fallback), domains (whoiser, RDAP
fallback), `AS<n>` (RDAP autnum); all carry a `__raw` block.
- **Offline datasets** (MaxMind, CAIDA, PeeringDB, IEEE MAC registries) are rows in
`common/datasets.js`, run by one engine,
`common/dataset-updater.js` (lock, atomic state, publish, boot download,
`server/datasets/datasets.js`, run by one engine,
`server/datasets/dataset-updater.js` (lock, atomic state, publish, boot download,
daily schedule). A row holds only its own logic — find the newest remote
version, fetch / distill it, validate, reload; a new dataset is a new row,
never a new updater. Fetched at boot when its reader has nothing usable
@@ -56,9 +62,9 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
reloaded by the engine's watcher. MaxMind only with its credentials (City + ASN publish together);
PeeringDB only with a Cloudflare key; its 116 MB dump is
stream-distilled (never parsed whole) to a ~6 MB per-ASN index without
contact data (`common/peeringdb-distill.js`). The IEEE row (MAC Lookup)
contact data (`server/datasets/peeringdb-distill.js`). The IEEE row (MAC Lookup)
needs no key: five registry CSVs published as downloaded, answered by the
longest covering assignment (`common/oui-db.js`).
longest covering assignment (`server/datasets/oui-db.js`).
## Conventions
@@ -67,7 +73,7 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
- **Every upstream call uses `fetchUpstream`** (`common/fetch-with-timeout.js`,
8s timeout), never a bare `fetch()` / `https.get()` — a hanging provider must
time out. It injects a default `User-Agent` `MyIP/v<version>/<VITE_SITE_URL>`
(`common/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied
(`server/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied
ones, incl. the private-API `{ ...req.headers }` pass-through, win.
- **Error shape.** `res.status(500).json({ error: error.message })` on upstream
failure, `400` on bad input. Terse — the frontend doesn't show them verbatim.
@@ -76,7 +82,7 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
sources match it. `timezone` is the exception (see "Response enrichment").
- **`?lang` is never validated in a handler.** Pass the raw tag through:
`lookupMaxMind` normalizes it onto the languages its data carries
(`SUPPORTED_LANGS` in `common/maxmind-service.js`); the private-API proxies
(`SUPPORTED_LANGS` in `server/datasets/maxmind-service.js`); the private-API proxies
forward it for the upstream's own resolution. No allow-lists.
- **Logging.** Shared logger only, `logger.error({ err, ...ctx }, 'msg')`; no
`console.*`, no "received request" lines (`pino-http` covers those).
@@ -87,14 +93,14 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`).
loaded; `SENTRY_ENVIRONMENT=development` skips the init. Handlers never import
Sentry: throws and 5xx traces are automatic; caught failures stay on the
logger (its hook mirrors warn+ to Sentry Logs, error+ to Issues). Periodic
jobs wrap their tick in `common/sentry-cron.js`; `key` / `token` query params
are redacted (`common/sentry-scrub.js`).
jobs wrap their tick in `server/sentry-cron.js`; `key` / `token` query params
are redacted (`server/sentry-scrub.js`).
## Security & Boundaries
### Guards live in middleware, not handlers
`common/guards.js`, attached in `backend-server.js`; handlers never repeat them:
`server/guards.js`, attached in `backend-server.js`; handlers never repeat them:
- `requireReferer` — global on `/api/*` (ALLOWED_DOMAINS + localhost).
- `requirePublicIP()` — per-route `?ip=`: well-formed and publicly routable;
@@ -120,7 +126,7 @@ New param shape → new guard there, attached in `backend-server.js`.
### Response enrichment lives in middleware too
`withTimeZone()` (`common/ip-timezone.js`), on all seven geo routes, derives
`withTimeZone()` (`server/ip-timezone.js`), on all seven geo routes, derives
`timezone` (IANA name) from the `latitude` / `longitude` the handler returned
(2xx only, the `cacheable` hook); no handler computes or forwards one. Own
coordinates keep the zone consistent with the city beside it; only the name
@@ -132,7 +138,7 @@ DST switch. A new geo source adds the middleware.
The backend listens before any missing offline dataset or the service-status
snapshot is fetched; snapshots already on disk load first, so only a first
boot waits on downloads. During that window `requireOfflineData([...probes])`
(`common/offline-data.js`) answers 503 on a route whose data is still missing
(`server/offline-data.js`) answers 503 on a route whose data is still missing
— never edge-cached — and is a no-op after boot, when a missing dataset means
a failed download served degraded. A route reading local data declares its
probes (`isMaxMindReady`, `isAsRelLoaded`, …) in `backend-server.js`. Answers
@@ -144,7 +150,7 @@ loading (`isStillLoading`) answers `error`, leaving the others to answer.
Handlers proxying our private IPCheck.ing API (`ipcheck-ing`,
`invisibility-test`, `update-user-achievement`, `get-user-info`,
`dns-leak-test`, `persona`, and `asn-profile`'s reputation via
`common/asn-reputation.js`) forward the caller's headers upstream (`headers: {
`server/asn-reputation.js`) forward the caller's headers upstream (`headers: {
...req.headers }`) — the upstream needs caller context (Accept-Language, auth
tokens). `persona` strips the framing headers (`host` / `content-length` / …)
first, as it re-serializes the body. Do **not** replicate for third-party
@@ -153,7 +159,7 @@ upstreams; those get only what's explicitly needed.
## Edge caching
Every `/api/*` response defaults to `Cache-Control: no-store`; slowly-changing
public routes opt in via `cacheable(maxAge)` (`common/cache-control.js`) in
public routes opt in via `cacheable(maxAge)` (`server/cache-control.js`) in
`backend-server.js`, e.g. `cacheable(24 * 60 * 60)` (TTLs written that way, not
raw seconds). `maxAge` may be a `(req) => seconds` resolver (cfradar: its view's
TTL; falsy = no-store). `{ cacheIf }` vetoes the full TTL for a 2xx body,
+9 -9
View File
@@ -8,21 +8,21 @@
// backbone connectivity is settlement-free peering with the Tier 1 clique,
// invisible to a p2c-only walk.
//
// Data is fully local (common/as-rel-db.js + common/as-org-db.js), so the
// Data is fully local (server/datasets/as-rel-db.js + server/datasets/as-org-db.js), so the
// whole BFS is synchronous. We only hit RIPEstat for as-overview as a
// rare fallback when as2org doesn't have an ASN's org name.
//
// Alongside the graph, `neighbours` lists the origin's direct providers,
// peers and customers from the same snapshot, counted by the walk's own
// misinference rule (common/as-relationships.js) so the counts never show an
// misinference rule (server/as-relationships.js) so the counts never show an
// upstream the graph drops. List names come from as2org, gaps filled from the
// graph's resolved names.
import { resolveAsnOrgName } from '../common/ripestat.js';
import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js';
import { lookupAsOrgName } from '../common/as-org-db.js';
import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../common/as-relationships.js';
import logger from '../common/logger.js';
import { resolveAsnOrgName } from '../server/ripestat.js';
import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../server/datasets/as-rel-db.js';
import { lookupAsOrgName } from '../server/datasets/as-org-db.js';
import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../server/as-relationships.js';
import logger from '../server/logger.js';
// How deep to recurse from the origin. 3 covers regional networks reaching
// Tier 1s through 1-2 intermediates; deeper just adds noise at the periphery.
@@ -33,10 +33,10 @@ const MAX_DEPTH = 3;
const MAX_INTERMEDIATE_BRANCH = 3;
// Past either bar (TIER1_PEERING_TRUSTED / TIER1_ADJACENCY_TRUSTED, defined
// in common/as-relationships.js so the neighbour counts use the same ones), a
// in server/as-relationships.js so the neighbour counts use the same ones), a
// node's non-Tier-1 "providers" are treated as CAIDA misinference and dropped.
// Two-tier org name resolver lives in common/ripestat.js. No onError hook
// Two-tier org name resolver lives in server/ripestat.js. No onError hook
// here — connectivity stays silent on as-overview fallback failures (a node
// just keeps name=null); asn-history is the one that warns.
const resolveOrgName = (asn) => resolveAsnOrgName(asn);
+3 -3
View File
@@ -13,8 +13,8 @@
import {
fetchRoutingHistory,
resolveAsnOrgName,
} from '../common/ripestat.js';
import logger from '../common/logger.js';
} from '../server/ripestat.js';
import logger from '../server/logger.js';
const prefixLength = (prefix) => parseInt((prefix || '').split('/')[1], 10);
@@ -56,7 +56,7 @@ function summarizeOrigin(entry, minLen) {
};
}
// Two-tier resolver lives in common/ripestat.js. Here we pass a warn hook so
// Two-tier resolver lives in server/ripestat.js. Here we pass a warn hook so
// a failed as-overview fallback stays observable (asn-connectivity omits it
// and stays silent — keep that difference).
const resolveOrgName = (asn) =>
+14 -14
View File
@@ -1,23 +1,23 @@
// /api/asn-profile?asn=<n> — everything the ASN Profile page shows, in one
// answer: Radar ASN summary + prefix list (common/cf-radar.js), CAIDA
// answer: Radar ASN summary + prefix list (server/cf-radar.js), CAIDA
// topology and neighbours (./asn-connectivity.js), RDAP autnum
// (common/rdap.js), ASRank (common/asrank.js), the local PeeringDB index
// (common/peeringdb-db.js) and, when the private API is configured,
// reputation (common/asn-reputation.js). Composition, deadlines and statuses
// live in common/asn-profile.js. `asn` arrives as a canonical number string
// (server/rdap.js), ASRank (server/asrank.js), the local PeeringDB index
// (server/datasets/peeringdb-db.js) and, when the private API is configured,
// reputation (server/asn-reputation.js). Composition, deadlines and statuses
// live in server/asn-profile.js. `asn` arrives as a canonical number string
// (requireValidASN). 200 unless every configured source failed (502).
import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../common/cf-radar.js';
import { rdapAutnum, isAutnumMissing } from '../common/rdap.js';
import { queryAsRank } from '../common/asrank.js';
import { requestAsnReputation } from '../common/asn-reputation.js';
import { isPeeringdbLoaded, lookupPeeringdb } from '../common/peeringdb-db.js';
import { isAsRelLoaded } from '../common/as-rel-db.js';
import { isAsOrgLoaded } from '../common/as-org-db.js';
import { isStillLoading } from '../common/offline-data.js';
import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../server/cf-radar.js';
import { rdapAutnum, isAutnumMissing } from '../server/rdap.js';
import { queryAsRank } from '../server/asrank.js';
import { requestAsnReputation } from '../server/asn-reputation.js';
import { isPeeringdbLoaded, lookupPeeringdb } from '../server/datasets/peeringdb-db.js';
import { isAsRelLoaded } from '../server/datasets/as-rel-db.js';
import { isAsOrgLoaded } from '../server/datasets/as-org-db.js';
import { isStillLoading } from '../server/offline-data.js';
import {
SOURCE_TIMEOUTS, buildSectionLoaders, composeAsnProfile, allSourcesFailed,
} from '../common/asn-profile.js';
} from '../server/asn-profile.js';
import { getAsnConnectivity } from './asn-connectivity.js';
// A section whose local data isn't there fails (status 'error'): left out
+3 -3
View File
@@ -1,12 +1,12 @@
// /api/cfradar — single dispatch route for all Cloudflare Radar data.
// `?view=` selects an entry in RADAR_VIEWS (common/cf-radar.js): the view's
// `?view=` selects an entry in RADAR_VIEWS (server/cf-radar.js): the view's
// guards validate and normalize its params, its fetch builds the payload,
// and the route's cache middleware in backend-server.js reads the view's
// per-view TTL. Guards run before the API-key check so param errors stay
// 400s on keyless deployments (and tests never reach an upstream call).
import { RADAR_VIEWS, hasRadarApiKey } from '../common/cf-radar.js';
import logger from '../common/logger.js';
import { RADAR_VIEWS, hasRadarApiKey } from '../server/cf-radar.js';
import logger from '../server/logger.js';
// Run a view's guard middlewares outside an Express chain. Each guard either
// calls next() or writes its own 4xx response — so "didn't pass" means the
+1 -1
View File
@@ -9,7 +9,7 @@
// GET /api/dnsleaktest/session/:token
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
const TOKEN_RE = /^[0-9a-f]{32}$/;
+1 -1
View File
@@ -3,7 +3,7 @@
// one flat, country-annotated result list the frontend groups by country.
import { Resolver } from 'node:dns/promises';
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
import { DNS_RESOLVERS } from './data/dns-resolvers.js';
import { NAME_VALUED_TYPES } from '../common/dns-record-types.js';
+1 -1
View File
@@ -1,5 +1,5 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
export default async (req, res) => {
const key = process.env.IPCHECKING_API_KEY;
+2 -2
View File
@@ -4,8 +4,8 @@
import whoiser from 'whoiser';
import { isValidIP, isUsablePublicIP } from '../common/valid-ip.js';
import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../common/rdap.js';
import logger from '../common/logger.js';
import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../server/rdap.js';
import logger from '../server/logger.js';
function isValidDomain(domain) {
const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;
+1 -1
View File
@@ -4,7 +4,7 @@
// Cloudflare the origin hits GitHub at most once per cache window — well under
// the 60 req/hour unauthenticated limit.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// The project's own repository. GitHub requires a User-Agent on every
// request; fetchUpstream's default project UA satisfies that.
+2 -2
View File
@@ -5,8 +5,8 @@
// bytes, probe-country coverage changes slowly.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import { buildProbeInventory } from '../common/globalping-inventory.js';
import logger from '../common/logger.js';
import { buildProbeInventory } from '../server/globalping-inventory.js';
import logger from '../server/logger.js';
const GLOBALPING_PROBES_URL = 'https://api.globalping.io/v1/probes';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// Validate request legitimacy
function isValidRequest(req) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// If length is not 28 and is not a combination of letters and numbers, return false
function isValidUserID(userID) {
+1 -1
View File
@@ -2,7 +2,7 @@
// Token-free upstream; normalizes the response into the canonical geo
// shape via the shared makeGeoHandler factory.
import { makeGeoHandler } from '../common/geo-handler.js';
import { makeGeoHandler } from '../server/geo-handler.js';
function buildUrl(req) {
const ipAddress = req.query.ip;
+1 -1
View File
@@ -2,7 +2,7 @@
// Picks a random API key and normalizes the response into the canonical
// geo shape via the shared makeGeoHandler factory.
import { makeGeoHandler } from '../common/geo-handler.js';
import { makeGeoHandler } from '../server/geo-handler.js';
function buildUrl(req) {
const ipAddress = req.query.ip;
+1 -1
View File
@@ -2,7 +2,7 @@
// Forwards the optional ?lang and normalizes the response into the
// canonical geo shape via the shared makeGeoHandler factory.
import { makeGeoHandler } from '../common/geo-handler.js';
import { makeGeoHandler } from '../server/geo-handler.js';
function buildUrl(req) {
const ipAddress = req.query.ip;
+1 -1
View File
@@ -2,7 +2,7 @@
// Picks a random API key and normalizes the response into the canonical
// geo shape (plus isHosting / isProxy) via the shared makeGeoHandler factory.
import { makeGeoHandler } from '../common/geo-handler.js';
import { makeGeoHandler } from '../server/geo-handler.js';
function buildUrl(req) {
const ipAddress = req.query.ip;
+1 -1
View File
@@ -1,5 +1,5 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
export default async (req, res) => {
// Presence, validity and public routability guaranteed by the
+1 -1
View File
@@ -3,7 +3,7 @@
// into the canonical geo shape via the shared makeGeoHandler factory.
import getCountryName from '../common/country-name.js';
import { makeGeoHandler } from '../common/geo-handler.js';
import { makeGeoHandler } from '../server/geo-handler.js';
function buildUrl(req) {
const ipAddress = req.query.ip;
+2 -2
View File
@@ -1,11 +1,11 @@
// /api/macchecker?mac= — MAC Lookup against the local IEEE registries
// (common/oui-db.js). `mac` is a full address or a vendor prefix of 6–12
// (server/datasets/oui-db.js). `mac` is a full address or a vendor prefix of 6–12
// hex digits (common/mac-input.js). The answer names the block that covers
// it, its range and the first-octet flags; an address no block covers is
// still a 200 with `found: false`.
import { normalizeMacQuery } from '../common/mac-input.js';
import { lookupMac } from '../common/oui-db.js';
import { lookupMac } from '../server/datasets/oui-db.js';
// `lookup` is injectable so tests can run the handler over fixture registries.
export const createMacChecker = (lookup) => async (req, res) => {
+2 -2
View File
@@ -1,5 +1,5 @@
import { lookupMaxMind } from '../common/maxmind-service.js';
import logger from '../common/logger.js';
import { lookupMaxMind } from '../server/datasets/maxmind-service.js';
import logger from '../server/logger.js';
export default (req, res) => {
// Presence, validity and public routability guaranteed by the
+3 -3
View File
@@ -1,7 +1,7 @@
// /api/ooni-blocking — per-country blocking view for a domain over the last
// 30 days, built from OONI's open aggregation API (web_connectivity test,
// probe_cc × blocking_type axes). Powers the default view of the frontend
// CensorshipCheck tool; classification lives in common/ooni-blocking.js.
// CensorshipCheck tool; classification lives in server/ooni-blocking.js.
//
// OONI matches `domain` against the exact tested hostname, so a single query
// misses most of the dataset (e.g. `bbc.com` has ~6 rows while `www.bbc.com`
@@ -13,8 +13,8 @@
// `cacheable()` and CF absorbs repeat lookups of popular domains.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../common/ooni-blocking.js';
import logger from '../common/logger.js';
import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../server/ooni-blocking.js';
import logger from '../server/logger.js';
const OONI_AGGREGATION_URL = 'https://api.ooni.io/api/v1/aggregation';
+1 -1
View File
@@ -7,7 +7,7 @@
// ids and enums, and the front end renders its own four-language copy.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// Headers that describe *this* hop rather than the caller, dropped before the
// request is rebuilt.
+1 -1
View File
@@ -7,7 +7,7 @@
// only when VITE_SENTRY_DSN_FRONTEND is set.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import { isValidIP } from '../common/valid-ip.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// The envelope header is the first newline-delimited JSON line and carries
// the DSN the SDK was configured with. Returns a URL or null. Exported for
+2 -2
View File
@@ -1,7 +1,7 @@
// /api/service-status — the "Service Status" section's read endpoints.
//
// Both handlers serve slices of the in-memory snapshot maintained by
// common/service-status-store.js (a background timer refreshes every provider
// server/service-status-store.js (a background timer refreshes every provider
// on a fixed 5-minute schedule). Neither touches an upstream — request volume
// has no effect on upstream load. Splitting overview vs per-provider detail
// keeps the initial page load light: the detail (sub-services + incidents) is
@@ -13,7 +13,7 @@
// The detail handler's `id` query param is whitelisted by
// requireValidProviderId() in backend-server.js.
import { getServiceStatusOverview, getProviderDetail } from '../common/service-status-store.js';
import { getServiceStatusOverview, getProviderDetail } from '../server/service-status-store.js';
// Overview: every provider's status light, no heavy detail arrays.
export default async (req, res) => {
+1 -1
View File
@@ -17,7 +17,7 @@
import crypto from 'node:crypto';
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import { validateReport, REPORT_TTL_DAYS, REPORT_MAX_BYTES } from '../common/report-schema.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
// --- env / KV plumbing -------------------------------------------------------
+1 -1
View File
@@ -1,5 +1,5 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import logger from '../server/logger.js';
export default async (req, res) => {
// defensive; app.put() in backend-server.js already gates method
+19 -19
View File
@@ -6,10 +6,10 @@ import { fileURLToPath } from 'url';
import { slowDown } from 'express-slow-down'
import rateLimit from 'express-rate-limit';
import pinoHttp from 'pino-http';
import logger from './common/logger.js';
import logger from './server/logger.js';
import { requireReferer, requirePublicIP, requireValidPrefix, requireValidASN, requireValidDomain, requireValidProviderId,
requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './common/guards.js';
import { withTimeZone } from './common/ip-timezone.js';
requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './server/guards.js';
import { withTimeZone } from './server/ip-timezone.js';
// Backend APIs
import mapHandler from './api/google-map.js';
@@ -23,7 +23,7 @@ import ipsbHandler from './api/ip-sb.js';
import maxmindHandler from './api/maxmind.js';
// Others
import cfRadarHandler from './api/cf-radar.js';
import { RADAR_VIEWS, isCompleteRadarAnswer } from './common/cf-radar.js';
import { RADAR_VIEWS, isCompleteRadarAnswer } from './server/cf-radar.js';
import asnHistoryHandler from './api/asn-history.js';
import asnConnectivityHandler from './api/asn-connectivity.js';
import ooniBlockingHandler from './api/ooni-blocking.js';
@@ -41,23 +41,23 @@ import macChecker from './api/mac-checker.js';
import githubStarsHandler from './api/github-stars.js';
import personaEvaluateHandler from './api/persona.js';
import asnProfileHandler from './api/asn-profile.js';
import { isCompleteProfile } from './common/asn-profile.js';
import { isCompleteProfile } from './server/asn-profile.js';
// User
import validateConfigs from './api/configs.js';
import getUserinfo from './api/get-user-info.js';
import updateUserAchievement from './api/update-user-achievement.js';
import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './common/maxmind-service.js';
import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './common/dataset-updater.js';
import { datasets } from './common/datasets.js';
import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './server/datasets/maxmind-service.js';
import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './server/datasets/dataset-updater.js';
import { datasets } from './server/datasets/datasets.js';
import {
bootstrapServiceStatus, startServiceStatusPolling, isServiceStatusPrimed,
} from './common/service-status-store.js';
import { isAsRelLoaded } from './common/as-rel-db.js';
import { isAsOrgLoaded } from './common/as-org-db.js';
import { isOuiLoaded } from './common/oui-db.js';
import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './common/offline-data.js';
import { cacheable } from './common/cache-control.js';
import { initUpstreamUserAgent } from './common/upstream-ua.js';
} from './server/service-status-store.js';
import { isAsRelLoaded } from './server/datasets/as-rel-db.js';
import { isAsOrgLoaded } from './server/datasets/as-org-db.js';
import { isOuiLoaded } from './server/datasets/oui-db.js';
import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './server/offline-data.js';
import { cacheable } from './server/cache-control.js';
import { initUpstreamUserAgent } from './server/upstream-ua.js';
dotenv.config({ quiet: true });
initUpstreamUserAgent();
@@ -216,17 +216,17 @@ if (speedLimitSet !== 0) {
app.use(express.json({ limit: '500kb' }));
// Default every /api/* response to no-store. Routes that want edge caching
// declare it explicitly via `cacheable(maxAge)` (common/cache-control.js).
// declare it explicitly via `cacheable(maxAge)` (server/cache-control.js).
app.use('/api', (req, res, next) => {
res.setHeader('Cache-Control', 'no-store');
next();
});
// Global referer gate for all /api/* routes. Handlers no longer repeat this
// check individually — see common/guards.js.
// check individually — see server/guards.js.
app.use('/api', requireReferer);
// Readiness gates for routes reading local data (common/offline-data.js):
// Readiness gates for routes reading local data (server/offline-data.js):
// 503 while that data is still downloading at boot, a no-op afterwards.
const needsMaxMind = requireOfflineData([isMaxMindReady]);
const needsAsGraph = requireOfflineData([isAsRelLoaded, isAsOrgLoaded]);
@@ -275,7 +275,7 @@ app.get('/api/asn-profile', requireValidASN(), cacheable(SEVEN_DAYS_CACHE, {
degradedMaxAge: () => (isOfflineBootstrapping() ? 0 : ONE_DAY_CACHE),
}), asnProfileHandler);
// All Cloudflare Radar data rides one route; `?view=` picks the dataset and
// the TTL comes from that view's registry entry (common/cf-radar.js) — 7d
// the TTL comes from that view's registry entry (server/cf-radar.js) — 7d
// for the ASN summary and prefix list (same as /api/asn-profile), 30d for
// country traffic, 1h for the outage feed. A partial answer (some upstream
// calls failed, or prefix countries MaxMind couldn't fully work out) is
+1 -1
View File
@@ -1,4 +1,4 @@
// The AS-number grammar shared by the ASN guards (common/guards.js) and the
// The AS-number grammar shared by the ASN guards (server/guards.js) and the
// tools that take a typed AS number (Whois, ASN Profile — via the
// frontend/utils/asn-input.js bridge): up to ten digits, AS1 … AS4294967295,
// `AS` prefix case-insensitive.
+1 -1
View File
@@ -10,7 +10,7 @@ const HEX_GROUP = /^[0-9a-fA-F]{1,4}$/;
// Expand an IPv6 address (with optional `::` compression) into 8 hextet
// strings. Returns null on any structural failure. Also consumed by
// common/rdap.js for CIDR containment math.
// server/rdap.js for CIDR containment math.
export function expandIPv6(ip) {
const halves = ip.split('::');
if (halves.length > 2) return null;
+1 -1
View File
@@ -36,7 +36,7 @@ export async function fetchWithTimeout(url, init = {}) {
}
// Optional User-Agent for fetchUpstream calls. Backend boot injects a
// project-identifying UA (see common/upstream-ua.js) because some upstream
// project-identifying UA (see server/upstream-ua.js) because some upstream
// WAFs hard-block undici's default `User-Agent: node`. Injection keeps this
// file browser-safe: the frontend bundle never touches fs / process.
let upstreamUserAgent = null;
+1 -1
View File
@@ -3,7 +3,7 @@
//
// The back end takes no part in that: which languages upstream *data* comes in
// is a separate set owned by the source itself (SUPPORTED_LANGS in
// common/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar
// server/datasets/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar
// tag onto its own family instead of rejecting it. A new UI locale is a
// front-end-only change.
+3 -3
View File
@@ -25,7 +25,7 @@ export const REPORT_TTL_DAYS = [1, 3, 7];
export const REPORT_MAX_BYTES = 256 * 1024;
// Anonymity verdict codes for ipinfo cards — made by
// utils/transform-ip-data.js, shared by the builder whitelist and the schema
// utils/ip/transform-ip-data.js, shared by the builder whitelist and the schema
// enum below.
export const ANONYMITY_CODES = [
'tor', 'relay', 'residential', 'suspected_residential',
@@ -33,7 +33,7 @@ export const ANONYMITY_CODES = [
];
// Connectivity status codes — single source shared by the recording component
// (ConnectivityTest.vue), the builder whitelist (utils/report-builders.js)
// (ConnectivityTest.vue), the builder whitelist (utils/report/report-builders.js)
// and the schema enum below, so a rename can't drift them apart.
export const CONNECTIVITY_STATUS = {
OK: 'ok',
@@ -165,7 +165,7 @@ const SECTION_SPECS = {
connectivity: obj({
testedAt: isoDate(),
// 72 = 7 built-in targets + the 60-target custom/import cap, plus
// headroom; matches the builder's slice in utils/report-builders.js.
// headroom; matches the builder's slice in utils/report/report-builders.js.
targets: arr(72, obj({
id: str(48),
name: str(64),
+1 -1
View File
@@ -4,7 +4,7 @@ import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { createProxyMiddleware } from 'http-proxy-middleware';
import logger from './common/logger.js';
import logger from './server/logger.js';
dotenv.config({ quiet: true });
+6 -2
View File
@@ -26,10 +26,14 @@ Every file opens with a header comment stating its purpose — read those.
- **Composition API** everywhere; no Options API. Alias `@` → `frontend/`.
- **Shared-with-backend helpers live in `common/`**, re-exported through a thin
`utils/` bridge so consumers keep `@/utils/...` imports (`utils/valid-ip.js`).
`utils/` bridge so consumers keep `@/utils/...` imports (`utils/ip/valid-ip.js`).
- **Helper placement:** Vue reactivity / lifecycle → `composables/` (`useXxx`);
otherwise `utils/` (never `use-` prefixed). `lib/` stays shadcn-only. A pure
function next to a composable exports from that composable's file.
- **Inside `utils/`:** app-wide plumbing at its root; IP / ASN data shaping
and validation → `ip/`; the report pipeline → `report/`; the pure layer of
one feature (a tool, a section, Pulse) → `features/`; per-source probe
modules keep their own dirs (`getips/`, `dnsleaks/`, `persona/`).
### Achievements are event-driven
@@ -39,7 +43,7 @@ events → slugs, `composables/use-achievement-engine.js` owns all guards (rules
wait for the remote snapshot; pre-sync hits parked). New achievement = entry +
rule + (only if no suitable event exists) a new event. The shareable report
rides the same bus: `<domain>:finished` → `use-report-collector.js` →
`utils/report-builders.js` → sections whitelisted by `common/report-schema.js`.
`utils/report/report-builders.js` → sections whitelisted by `common/report-schema.js`.
New reportable test = event + builder + schema entry in one change; new result
semantics = builder whitelist + schema enum too (builders fail soft, fixtures
are frozen: drift = quietly missing fields). Report links are public: the
+2 -2
View File
@@ -17,7 +17,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
import Alert from '@/components/widgets/Toast.vue';
import DocsAssistant from '@/components/widgets/DocsAssistant.vue';
import { shouldOfferPwaInstall } from '@/utils/pwa.js';
import { sendVisitBeacon } from '@/utils/pulse-beacon.js';
import { sendVisitBeacon } from '@/utils/features/pulse-beacon.js';
import { useTheme } from '@/composables/use-theme.js';
// PWA install prompt — async and eligibility-gated: ineligible visits (too
@@ -31,7 +31,7 @@ onMounted(() => {
setTimeout(() => { offerPwaInstall.value = true; }, 30 * 1000);
}
// Earth Online visit tick — app-level so every route counts, once per
// page load; the backend dedups per IP (utils/pulse-beacon.js).
// page load; the backend dedups per IP (utils/features/pulse-beacon.js).
sendVisitBeacon();
});
import { useAchievementEngine } from '@/composables/use-achievement-engine.js';
+3 -3
View File
@@ -160,9 +160,9 @@ import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { emitAppEvent, waitForAppEvent } from '@/utils/app-events';
import { useAppCommand } from '@/composables/use-app-command.js';
import { CONNECTIVITY_STATUS } from '@/utils/report-schema.js';
import { CONNECTIVITY_STATUS } from '@/utils/report/report-schema.js';
import { TILE_PREVIEW, faviconPath, MINE_LIST_ID } from '@/data/connectivity-import-lists.js';
import { siteUrlOf, removeMember } from '@/utils/connectivity-lists.js';
import { siteUrlOf, removeMember } from '@/utils/features/connectivity-lists.js';
import { JnTooltip } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
@@ -386,7 +386,7 @@ const checkConnectivityHandler = async (test, onTestComplete = () => { }, isManu
const testTime = Math.round(performance.now() - beginTime);
test.status = t('connectivity.StatusAvailable');
// Locale-free twin of the localized status label, consumed by the report
// builder (utils/report-builders.js) — the label itself can't be mapped
// builder (utils/report/report-builders.js) — the label itself can't be mapped
// back to an enum without string comparison against t() output.
test.statusCode = CONNECTIVITY_STATUS.OK;
test.mintime = test.mintime === 0 ? testTime : Math.min(test.mintime, testTime);
+2 -2
View File
@@ -35,9 +35,9 @@ import { ref, computed, onMounted, reactive, watch } from 'vue';
import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { isUsablePublicIP } from '@/utils/valid-ip.js';
import { isUsablePublicIP } from '@/utils/ip/valid-ip.js';
import { toApiTag } from '@/utils/locale-registry.js';
import { transformDataFromIPapi } from '@/utils/transform-ip-data.js';
import { transformDataFromIPapi } from '@/utils/ip/transform-ip-data.js';
import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips';
import { emitAppEvent, waitForAppEvent } from '@/utils/app-events';
import { useAppCommand } from '@/composables/use-app-command.js';
+3 -3
View File
@@ -184,11 +184,11 @@ import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { useAppCommand } from '@/composables/use-app-command.js';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { isValidIP } from '@/utils/valid-ip.js';
import { parseTrace } from '@/utils/parse-trace.js';
import { isValidIP } from '@/utils/ip/valid-ip.js';
import { parseTrace } from '@/utils/ip/parse-trace.js';
import getCountryName from '@/data/country-name.js';
import SpeedTestEngine from '@cloudflare/speedtest';
import { createSpeedTestSession, getSpeedTestLiveValues } from '@/utils/speedtest-session.js';
import { createSpeedTestSession, getSpeedTestLiveValues } from '@/utils/features/speedtest-session.js';
import useSpeedTestCharts from '@/composables/use-speedtest-charts.js';
import { JnTooltip } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
@@ -1,6 +1,6 @@
<!-- ASN Profile (advanced tool, slug `asn`): everything known about one AS
number, from one request to /api/asn-profile (the backend gathers every
source and answers once; see common/asn-profile.js). The page shows one
source and answers once; see server/asn-profile.js). The page shows one
loading state, then renders whole; a section whose source had nothing or
failed is simply left out.
Order: hero (identity, key facts, registration) beside a compact
@@ -10,7 +10,7 @@
One source per concept: relationships = local CAIDA (graph, counts and
lists alike); prefixes, addresses and RPKI = the pfx2as list; identity =
Radar → RDAP → ASRank. The query rides the URL as `?q=` (shareable,
written back on every run). Shaping lives in utils/asn-profile-view.js. -->
written back on every run). Shaping lives in utils/ip/asn-profile-view.js. -->
<template>
<div class="my-4 space-y-4">
<!-- Top note -->
@@ -429,8 +429,8 @@ import { ChevronRight, Download, ExternalLink, Globe, Search, ShieldAlert, Shiel
import { trackEvent } from '@/utils/analytics';
import { useStatusTone } from '@/composables/use-status-tone.js';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { parseAsnInput } from '@/utils/asn-input.js';
import { buildTrafficPairs, pickConnectionQuality } from '@/utils/asn-metrics.js';
import { parseAsnInput } from '@/utils/ip/asn-input.js';
import { buildTrafficPairs, pickConnectionQuality } from '@/utils/ip/asn-metrics.js';
import {
asnProfileUrl, ASN_PROFILE_TIMEOUT_MS, sectionData, isProfileEmpty, failedSources,
hasTopology, isTier1Origin, heroIdentity, registrationFields, keyFacts, coneAsns,
@@ -438,7 +438,7 @@ import {
countryShareRows, neighbourGroups,
BASELINE_POSITION, reputationMeters, proxyListedCount, verdictLevel, VERDICT_TONE,
peeringEnumKey, peeringHero, peeringFacts, formatPortSpeed, exchangeRows, facilityRows, hasPeeringCard, visibleRows,
} from '@/utils/asn-profile-view.js';
} from '@/utils/ip/asn-profile-view.js';
import { formatIsoDate } from '@/utils/time-utils.js';
import getCountryName from '@/data/country-name.js';
import AsnConnectionQuality from '@/components/ip-infos/AsnConnectionQuality.vue';
@@ -186,7 +186,7 @@ import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { useGlobalpingMeasurement } from '@/composables/use-globalping-measurement';
import GlobalpingCountryPicker from './GlobalpingCountryPicker.vue';
import { isValidDomain } from '@/utils/valid-ip.js';
import { isValidDomain } from '@/utils/ip/valid-ip.js';
import getCountryName from '@/data/country-name.js';
import { formatIsoDate } from '@/utils/time-utils.js';
import { Input } from '@/components/ui/input';
@@ -213,7 +213,7 @@ const MAX_TEST_COUNTRIES = 15;
const PROBES_PER_COUNTRY = 2;
// OONI tier / blocking-method display maps (tiers computed by the backend,
// see common/ooni-blocking.js).
// see server/ooni-blocking.js).
const TIER_META = {
confirmed: { labelKey: 'TierConfirmed', badgeClass: 'bg-destructive/15 text-destructive border-transparent' },
likely: { labelKey: 'TierLikely', badgeClass: 'bg-warning/15 text-warning border-transparent' },
@@ -110,8 +110,8 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Icon } from '@iconify/vue';
import { trackEvent } from '@/utils/analytics';
import { isValidDomain } from '@/utils/valid-ip.js';
import { DNS_RECORD_TYPES } from '@/utils/dns-record-types.js';
import { isValidDomain } from '@/utils/ip/valid-ip.js';
import { DNS_RECORD_TYPES } from '@/utils/features/dns-record-types.js';
import getCountryName from '@/data/country-name.js';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@@ -185,7 +185,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { Icon } from '@iconify/vue';
import { Info, Play } from '@lucide/vue';
import { renderWorldMapChart } from '@/utils/world-map-chart.js';
import { renderWorldMapChart } from '@/utils/features/world-map-chart.js';
const { t } = useI18n();
@@ -47,7 +47,7 @@
import { ref, computed, onMounted } from 'vue';
import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { getProbeInventory } from '@/utils/globalping-probes.js';
import { getProbeInventory } from '@/utils/features/globalping-probes.js';
import getCountryName from '@/data/country-name.js';
import { Checkbox } from '@/components/ui/checkbox';
import { Icon } from '@iconify/vue';
@@ -1,7 +1,7 @@
<!-- IP Calculator (advanced tool, slug `ipcalculator`): one smart input
(IPv4 / IPv6, prefix, number, range, prefix list — never a MAC, that is
MAC Lookup's job) dispatched to the matching card under ./ip-calculator/.
Pure local computation via utils/ip-calc.js.
Pure local computation via utils/features/ip-calc.js.
The query rides the URL as `?q=` on both /tools/ipcalculator and
/?tool=ipcalculator, written back on every run so results are shareable.
@@ -92,9 +92,9 @@ import { Icon } from '@iconify/vue';
import { Calculator, ChevronRight } from '@lucide/vue';
import { useMainStore } from '@/store';
import { trackEvent } from '@/utils/analytics';
import { isValidIP } from '@/utils/valid-ip.js';
import { isValidIP } from '@/utils/ip/valid-ip.js';
import { selectableIPs } from '@/composables/use-globalping-measurement.js';
import { analyzeCidr, calculate } from '@/utils/ip-calc.js';
import { analyzeCidr, calculate } from '@/utils/features/ip-calc.js';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
@@ -117,7 +117,7 @@ import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import getCountryName from '@/data/country-name.js';
import { normalizeMacQuery } from '@/utils/mac-input.js';
import { normalizeMacQuery } from '@/utils/features/mac-input.js';
import { ChevronRight, CircleCheck, CircleX, Factory, ListChecks, Search } from '@lucide/vue';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@@ -201,7 +201,7 @@ import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events';
import { useGlobalpingMeasurement, GLOBALPING_SUGGESTED_COUNTRIES, GLOBALPING_MAX_COUNTRIES, selectableIPs, classifyTarget } from '@/composables/use-globalping-measurement';
import GlobalpingCountryPicker from './GlobalpingCountryPicker.vue';
import { parseMtrOutput } from '@/utils/mtr-parse.js';
import { parseMtrOutput } from '@/utils/features/mtr-parse.js';
import getCountryName from '@/data/country-name.js';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
import { Card, CardContent } from '@/components/ui/card';
@@ -433,7 +433,7 @@ const run = async () => {
{ persona: persona.value, observation });
runStatus.value = 'finished';
// The shareable report keeps only each check's conclusion — the
// builder in utils/report-builders.js drops every detail field.
// builder in utils/report/report-builders.js drops every detail field.
emitAppEvent('persona:finished', {
country: persona.value.country,
grade: report.value.grade,
@@ -85,7 +85,7 @@ import { ref, computed, onMounted, watch } from 'vue';
import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { emitAppEvent } from '@/utils/app-events.js';
import { parseTrace } from '@/utils/parse-trace.js';
import { parseTrace } from '@/utils/ip/parse-trace.js';
import getCountryName from '@/data/country-name.js';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
@@ -178,7 +178,7 @@ import { useStatusTone } from '@/composables/use-status-tone.js';
import { unixToDateTime } from '@/utils/time-utils.js';
import {
indicatorToTone, componentStatusToTone, impactLevel, incidentStatusTone,
} from '@/utils/service-status-tone.js';
} from '@/utils/features/service-status-tone.js';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
+2 -2
View File
@@ -66,8 +66,8 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { isValidIP, isValidDomain, isUsablePublicIP } from '@/utils/valid-ip.js';
import { parseAsnInput } from '@/utils/asn-input.js';
import { isValidIP, isValidDomain, isUsablePublicIP } from '@/utils/ip/valid-ip.js';
import { parseAsnInput } from '@/utils/ip/asn-input.js';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@@ -78,7 +78,7 @@ import CalcSection from './CalcSection.vue';
import ValueRow from './ValueRow.vue';
import PrefixBitmap from './PrefixBitmap.vue';
import SubnetSplitter from './SubnetSplitter.vue';
import { countLabel } from '@/utils/ip-calc.js';
import { countLabel } from '@/utils/features/ip-calc.js';
const props = defineProps({
analysis: { type: Object, required: true },
@@ -90,7 +90,7 @@ import CalcSection from './CalcSection.vue';
import ValueRow from './ValueRow.vue';
import PrefixBitmap from './PrefixBitmap.vue';
import SubnetSplitter from './SubnetSplitter.vue';
import { countLabel } from '@/utils/ip-calc.js';
import { countLabel } from '@/utils/features/ip-calc.js';
const props = defineProps({
analysis: { type: Object, required: true },
@@ -42,7 +42,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { Slider } from '@/components/ui/slider';
import { toOctets, toHextets } from '@/utils/ip-math.js';
import { toOctets, toHextets } from '@/utils/ip/ip-math.js';
const props = defineProps({
value: { type: BigInt, required: true },
@@ -47,7 +47,7 @@ import { Card, CardContent } from '@/components/ui/card';
import CopyButton from '@/components/widgets/CopyButton.vue';
import CalcSection from './CalcSection.vue';
import ValueRow from './ValueRow.vue';
import { countLabel } from '@/utils/ip-calc.js';
import { countLabel } from '@/utils/features/ip-calc.js';
const props = defineProps({
analysis: { type: Object, required: true },
@@ -39,8 +39,8 @@ import { Split } from '@lucide/vue';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import CopyButton from '@/components/widgets/CopyButton.vue';
import CalcSection from './CalcSection.vue';
import { splitCidr } from '@/utils/ip-math.js';
import { countLabel, formatCount } from '@/utils/ip-calc.js';
import { splitCidr } from '@/utils/ip/ip-math.js';
import { countLabel, formatCount } from '@/utils/features/ip-calc.js';
const props = defineProps({
cidr: { type: String, required: true },
+1 -1
View File
@@ -55,7 +55,7 @@ import AsnConnectionQuality from './AsnConnectionQuality.vue';
import AsnTrafficShares from './AsnTrafficShares.vue';
import { useRouter } from 'vue-router';
import { trackEvent } from '@/utils/analytics';
import { parseAsnInput } from '@/utils/asn-input.js';
import { parseAsnInput } from '@/utils/ip/asn-input.js';
import { isToolAvailable } from '@/utils/tool-availability.js';
import { TOOL_BY_SLUG } from '@/data/tools.js';
import { Button } from '@/components/ui/button';
@@ -23,7 +23,7 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { pickConnectionQuality } from '@/utils/asn-metrics.js';
import { pickConnectionQuality } from '@/utils/ip/asn-metrics.js';
import { JnTooltip } from '@/components/ui/tooltip';
import { CircleQuestionMark } from '@lucide/vue';
@@ -15,7 +15,7 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { buildTrafficPairs } from '@/utils/asn-metrics.js';
import { buildTrafficPairs } from '@/utils/ip/asn-metrics.js';
import DataPairBar from './DataPairBar.vue';
const props = defineProps({
+1 -1
View File
@@ -65,7 +65,7 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { isValidIP } from '@/utils/valid-ip.js';
import { isValidIP } from '@/utils/ip/valid-ip.js';
import FitText from '@/components/widgets/FitText.vue';
import { HERO_TIERS } from '@/composables/use-fit-text.js';
import { createMaskGate } from '@/composables/use-info-mask.js';
@@ -314,10 +314,10 @@ import { useI18n, I18nT } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { loadAsnInfoInto } from '@/composables/use-asn-info.js';
import { loadAsnConnectivityInto } from '@/utils/asn-connectivity.js';
import { toBgpPrefix } from '@/utils/bgp-prefix.js';
import { loadAsnConnectivityInto } from '@/utils/ip/asn-connectivity.js';
import { toBgpPrefix } from '@/utils/ip/bgp-prefix.js';
import { getZoneUtcOffset, getZoneLocalTime, formatIsoDate } from '@/utils/time-utils.js';
import { buildScoreExplanation, listScoreTags } from '@/utils/ip-score-details.js';
import { buildScoreExplanation, listScoreTags } from '@/utils/ip/ip-score-details.js';
import ASNInfo from './ASNInfo.vue';
import ASNHistory from './ASNHistory.vue';
import CountryTraffic from './CountryTraffic.vue';
@@ -606,7 +606,7 @@ const getASNHistory = async (prefix) => {
}
};
// Fetch + versioning live in utils/asn-connectivity.js; the cache is the
// Fetch + versioning live in utils/ip/asn-connectivity.js; the cache is the
// owner's, handed down as asnConnectivityInfos.
const getASNConnectivity = async (asn) => {
trackEvent('IPCheck', 'ASNConnectivityClick', 'Show ASN Connectivity');
+2 -2
View File
@@ -68,8 +68,8 @@ import { useI18n } from 'vue-i18n';
import { useMainStore } from '@/store';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { isoToDateTime } from '@/utils/time-utils.js';
import { REPORT_VERSION, REPORT_SECTION_IDS } from '@/utils/report-schema.js';
import { reportToMarkdown } from '@/utils/report-export.js';
import { REPORT_VERSION, REPORT_SECTION_IDS } from '@/utils/report/report-schema.js';
import { reportToMarkdown } from '@/utils/report/report-export.js';
import { useDocumentMeta } from '@/composables/use-document-meta.js';
import StandalonePageHeader from '@/components/StandalonePageHeader.vue';
import Footer from '@/components/Footer.vue';
@@ -124,8 +124,8 @@ import { useMainStore } from '@/store';
import { trackEvent } from '@/utils/analytics';
import { useCollectedReport } from '@/composables/use-report-collector.js';
import { useReportShare } from '@/composables/use-report-share.js';
import { REPORT_SECTION_IDS, REPORT_TTL_DAYS } from '@/utils/report-schema.js';
import { SECTION_TITLE_KEYS, buildShareReport, reportToMarkdown, downloadReportJson } from '@/utils/report-export.js';
import { REPORT_SECTION_IDS, REPORT_TTL_DAYS } from '@/utils/report/report-schema.js';
import { SECTION_TITLE_KEYS, buildShareReport, reportToMarkdown, downloadReportJson } from '@/utils/report/report-export.js';
import { isoToDateTime } from '@/utils/time-utils.js';
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
@@ -66,7 +66,7 @@ import { useI18n } from 'vue-i18n';
import GeoCell from './GeoCell.vue';
import { TH_LEFT, TH_RIGHT, TD_NUM } from './table-classes.js';
// Anonymity verdict code → the IP card's own label.
import { ANONYMITY_I18N_KEYS } from '@/utils/transform-ip-data.js';
import { ANONYMITY_I18N_KEYS } from '@/utils/ip/transform-ip-data.js';
const props = defineProps({ section: { type: Object, required: true } });
const { t } = useI18n();
@@ -16,7 +16,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { Card, CardContent } from '@/components/ui/card';
import { SECTION_TITLE_KEYS } from '@/utils/report-export.js';
import { SECTION_TITLE_KEYS } from '@/utils/report/report-export.js';
import { isoToDateTime } from '@/utils/time-utils.js';
const props = defineProps({
@@ -112,7 +112,7 @@
</TabsContent>
<!-- List management: create, rename, reorder, delete. Guards live
in utils/connectivity-lists.js. -->
in utils/features/connectivity-lists.js. -->
<TabsContent value="lists" class="space-y-3">
<div class="flex items-center gap-2">
<Input id="new-conn-list" v-model="newListName" :placeholder="t('connectivity.lists.NewListPlaceholder')"
@@ -184,7 +184,7 @@
// Tabbed dialog behind the Connectivity "Add Test" tile and the header
// menu's manage entry: import a curated list, hand-add a custom target, or
// manage the lists themselves. All mutations go through the pure ops in
// utils/connectivity-lists.js and persist into connectivityLists.
// utils/features/connectivity-lists.js and persist into connectivityLists.
import { ref, computed, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMainStore } from '@/store';
@@ -195,7 +195,7 @@ import {
} from '@/data/connectivity-import-lists.js';
import {
isListFullyPresent, importIntoList, importAsNewList, addMember, createList, renameList, moveList, deleteList,
} from '@/utils/connectivity-lists.js';
} from '@/utils/features/connectivity-lists.js';
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+1 -1
View File
@@ -173,7 +173,7 @@ import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { useIpHistory } from '@/composables/use-ip-history.js';
import { INLINE_TIERS } from '@/composables/use-fit-text.js';
import { filterHistoryDays, groupHistoryByIP, countryFacets, ipVersionCounts } from '@/utils/ip-history.js';
import { filterHistoryDays, groupHistoryByIP, countryFacets, ipVersionCounts } from '@/utils/ip/ip-history.js';
import { formatIsoDate } from '@/utils/time-utils.js';
import getCountryName from '@/data/country-name.js';
import FitText from '@/components/widgets/FitText.vue';
+1 -1
View File
@@ -196,7 +196,7 @@ import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { clampRetentionDays } from '@/utils/ip-history.js';
import { clampRetentionDays } from '@/utils/ip/ip-history.js';
import { LOCALES } from '@/utils/locale-registry.js';
import { MINE_LIST_ID } from '@/data/connectivity-import-lists.js';
import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet';
+7 -7
View File
@@ -16,7 +16,7 @@
<SheetContent side="right" :title="t('nav.pulse.title')"
class="flex w-full max-w-full flex-col gap-0 p-0 md:w-125 md:max-w-125">
<!-- Celebration overlay: one canvas above the whole Sheet, drawn
by utils/pulse-celebration.js. Inert to input. -->
by utils/features/pulse-celebration.js. Inert to input. -->
<canvas ref="fxCanvas" class="pointer-events-none absolute inset-0 z-10 size-full"
aria-hidden="true"></canvas>
<header class="flex shrink-0 items-center justify-between gap-2 border-b px-4 py-3">
@@ -227,17 +227,17 @@
// The entry button shows when either half can render.
// POST <PULSE_BEACON_URL>/status on pick; GET <PULSE_BEACON_URL>/stats on
// open and after a send — uncached end to end. The visit beacon is app-level:
// App.vue via utils/pulse-beacon.js, not this widget.
// App.vue via utils/features/pulse-beacon.js, not this widget.
import { ref, computed, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMainStore } from '@/store';
import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { PULSE_BEACON_URL, hasPulseBackend } from '@/utils/pulse-beacon.js';
import { PULSE_BEACON_URL, hasPulseBackend } from '@/utils/features/pulse-beacon.js';
import { PRESET_STATUSES, FESTIVAL_STATUSES, festivalsActiveOn, localDateString } from '@/data/pulse-statuses.js';
import { playCelebration, resolveEffect } from '@/utils/pulse-celebration.js';
import { renderWorldMapChart, preloadWorldMapChart } from '@/utils/world-map-chart.js';
import { playCelebration, resolveEffect } from '@/utils/features/pulse-celebration.js';
import { renderWorldMapChart, preloadWorldMapChart } from '@/utils/features/world-map-chart.js';
import { relativeTimeFromMinutes } from '@/utils/time-utils.js';
import getCountryName from '@/data/country-name.js';
import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet';
@@ -359,7 +359,7 @@ const showHint = (kind) => {
hintKind.value = kind;
hintStamp.value += 1;
};
// Celebration overlay (utils/pulse-celebration.js): one canvas over the
// Celebration overlay (utils/features/pulse-celebration.js): one canvas over the
// Sheet, played optimistically on click — delight belongs to the tap, not
// the round-trip. A 429 stops it immediately (the send didn't count); a new
// click restarts it. The engine no-ops under prefers-reduced-motion.
@@ -430,7 +430,7 @@ const sendStatus = async (id, evt) => {
}
};
// World map via the shared choropleth util (utils/world-map-chart.js) —
// World map via the shared choropleth util (utils/features/world-map-chart.js) —
// heat ramp (warm orange → deep red), fitting the "heatmap" framing; T1 has
// no territory to shade.
const mapCanvas = ref(null);
+2 -2
View File
@@ -64,11 +64,11 @@
// asnInfos comes from useAsnInfo().
import { ref, computed, watch, nextTick } from 'vue';
import { useMainStore } from '@/store';
import { isValidIP, isUsablePublicIP } from '@/utils/valid-ip.js';
import { isValidIP, isUsablePublicIP } from '@/utils/ip/valid-ip.js';
import { toApiTag } from '@/utils/locale-registry.js';
import FitText from '@/components/widgets/FitText.vue';
import { HERO_TIERS } from '@/composables/use-fit-text.js';
import { transformDataFromIPapi } from '@/utils/transform-ip-data.js';
import { transformDataFromIPapi } from '@/utils/ip/transform-ip-data.js';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import { authenticatedFetch, fetchErrorLabel, logSourceFetchFailure } from '@/utils/authenticated-fetch';
+3 -3
View File
@@ -40,10 +40,10 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useMainStore } from '@/store';
import { useCollectedReport } from '@/composables/use-report-collector.js';
import { REPORT_SECTION_IDS } from '@/utils/report-schema.js';
import { SECTION_TITLE_KEYS } from '@/utils/report-export.js';
import { REPORT_SECTION_IDS } from '@/utils/report/report-schema.js';
import { SECTION_TITLE_KEYS } from '@/utils/report/report-export.js';
import { dispatchAppCommand, waitForAppCommand } from '@/utils/app-commands.js';
import { RUNNABLE_SECTION_COMMANDS, RUNNABLE_SECTION_IDS, normalizeRunSections } from '@/utils/docs-run-tests.js';
import { RUNNABLE_SECTION_COMMANDS, RUNNABLE_SECTION_IDS, normalizeRunSections } from '@/utils/report/docs-run-tests.js';
export const DOCS_URL = (import.meta.env?.VITE_DOCS_URL || '').replace(/\/+$/, '');
export const isDocsConfigured = !!DOCS_URL;
@@ -25,12 +25,12 @@
import { ref, onScopeDispose } from 'vue';
import { fetchWithTimeout } from '../../common/fetch-with-timeout.js';
import { isValidIP, isUsablePublicIP } from '../utils/valid-ip.js';
import { isValidIP, isUsablePublicIP } from '../utils/ip/valid-ip.js';
// Curated 28-country spread shared by the MtrTest and GlobalLatencyTest
// tools: their picker's "suggested" section and default selection. The full
// per-continent catalog comes from the live probe inventory
// (utils/globalping-probes.js); CensorshipCheck keeps its own suggestion
// (utils/features/globalping-probes.js); CensorshipCheck keeps its own suggestion
// lists because its sections are censorship-oriented, not geographic.
export const GLOBALPING_SUGGESTED_COUNTRIES = [
'HK', 'TW', 'CN', 'JP', 'KR', 'MY', 'ID', 'SG', 'IN', 'SA',
+2 -2
View File
@@ -2,7 +2,7 @@
// localStorage history grouped by day, and exposes it reactively for the
// IPHistory panel. Local-only by design: the record never syncs to the user's
// account. Gated by user preferences: ipHistoryEnabled stops recording,
// ipHistoryDays (1–30) sets retention. Pure logic lives in utils/ip-history.js.
// ipHistoryDays (1–30) sets retention. Pure logic lives in utils/ip/ip-history.js.
import { ref, computed, watch } from 'vue';
import { emitAppEvent } from '@/utils/app-events.js';
import {
@@ -15,7 +15,7 @@ import {
localDayKey,
clampRetentionDays,
distinctCountryCount,
} from '@/utils/ip-history.js';
} from '@/utils/ip/ip-history.js';
export const useIpHistory = ({ store }) => {
const enabled = computed(() => store.userPreferences.ipHistoryEnabled !== false);
+1 -1
View File
@@ -14,7 +14,7 @@
import { useMainStore } from '../store.js';
import { useI18n } from 'vue-i18n';
import { fetchWithTimeout } from '../utils/fetch-with-timeout.js';
import { transformDataFromIPapi } from '../utils/transform-ip-data.js';
import { transformDataFromIPapi } from '../utils/ip/transform-ip-data.js';
import getCountryName from '../data/country-name.js';
import { toApiTag } from '../utils/locale-registry.js';
@@ -14,7 +14,7 @@
import { reactive, computed, onScopeDispose } from 'vue';
import { onAppEvent } from '../utils/app-events.js';
import { isValidIP, isIPv6 } from '../utils/valid-ip.js';
import { isValidIP, isIPv6 } from '../utils/ip/valid-ip.js';
import { observeBrowser } from '../utils/persona/observe-browser.js';
import { probeFonts } from '../utils/persona/probe-fonts.js';
import { probeVoices, probeKeyboard } from '../utils/persona/probe-locale.js';
+2 -2
View File
@@ -1,13 +1,13 @@
// Report collector — subscribes to the test-completion events on the app bus
// (utils/app-events.js), runs each payload through its report builder
// (utils/report-builders.js) and keeps the latest schema-shaped snapshot per
// (utils/report/report-builders.js) and keeps the latest schema-shaped snapshot per
// section. This is the data source for the share-report dialog; components
// stay report-agnostic, exactly like the achievement engine's pattern.
// Call once from App.vue setup.
import { reactive, computed, onScopeDispose } from 'vue';
import { onAppEvent } from '../utils/app-events.js';
import { REPORT_EVENT_BUILDERS } from '../utils/report-builders.js';
import { REPORT_EVENT_BUILDERS } from '../utils/report/report-builders.js';
// Module-level so later consumers (share dialog) see the same snapshots the
// App.vue-mounted subscriber collected. Shape: sectionId → schema section
+1 -1
View File
@@ -28,7 +28,7 @@ import { emitAppEvent } from '../utils/app-events.js';
import { dispatchAppCommand } from '../utils/app-commands.js';
import { registerShortcuts, keyMap, navigateCards } from '../utils/shortcut.js';
import { scrollToElement } from '../utils/scroll-to.js';
import { hasPulseBackend } from '../utils/pulse-beacon.js';
import { hasPulseBackend } from '../utils/features/pulse-beacon.js';
// A shortcut only kicks the run off — completion is the owner's business —
// so a failed dispatch just logs.
+1 -1
View File
@@ -1,6 +1,6 @@
// Buffer speed-test samples independently of the asynchronously loaded charts.
import { ref, reactive } from 'vue';
import { getSpeedTestSampleCounts } from '../utils/speedtest-session.js';
import { getSpeedTestSampleCounts } from '../utils/features/speedtest-session.js';
// Extract Chart.js related configurations
const getChartConfig = (t) => ({
+1 -1
View File
@@ -2,7 +2,7 @@
// chart on its canvas so a closing animation can finish; destroy when the
// canvas leaves or the scope is disposed.
import { ref, watch, onScopeDispose, toValue } from 'vue';
import { renderWorldMapChart } from '../utils/world-map-chart.js';
import { renderWorldMapChart } from '../utils/features/world-map-chart.js';
export const useWorldMapChart = ({ canvas, visible, options, theme }, { render = renderWorldMapChart } = {}) => {
const ready = ref(false);
+1 -1
View File
@@ -3,7 +3,7 @@
// `effect` on festivals ('fireworks' | 'fall'; absent → the default emoji
// pop). `effectEmoji` overrides the particle sprite when the status emoji
// isn't the thing that should fall (christmas 🎄 snows ❄️). Recipes live in
// utils/pulse-celebration.js.
// utils/features/pulse-celebration.js.
export const PRESET_STATUSES = [
{ id: 'fast', emoji: '🚀' },
+1 -1
View File
@@ -8,7 +8,7 @@
// event on utils/app-events.js and subscribe to it here instead.
import * as Sentry from '@sentry/vue';
import { onAppEvent } from '@/utils/app-events';
import { isValidIP } from '@/utils/valid-ip.js';
import { isValidIP } from '@/utils/ip/valid-ip.js';
const env = import.meta.env ?? {};
+1 -1
View File
@@ -6,7 +6,7 @@ import i18n from './locales/i18n.js';
import { createInitialAchievementsState } from './data/achievements.js';
import { createInitialIpDBs, buildDbUrl, applyConfigAvailability, nearestEnabledId } from './data/ip-databases.js';
import { createDefaultPreferences, PREFS_STORAGE_KEY } from './data/default-preferences.js';
import { sanitizeLists } from './utils/connectivity-lists.js';
import { sanitizeLists } from './utils/features/connectivity-lists.js';
import { createMountingStatus, createLoadingStatus, DEFAULT_SECTION } from './data/sections.js';
import { fetchWithTimeout } from './utils/fetch-with-timeout.js';
const { t } = i18n.global;
-3
View File
@@ -1,3 +0,0 @@
// Thin re-export of common/asn-input.js (shared with the ASN guards), so
// front-end code keeps importing `@/utils/asn-input.js`.
export { MAX_ASN, parseAsnInput } from '../../common/asn-input.js';
+1 -1
View File
@@ -7,7 +7,7 @@
// store dependency — the consuming component (DnsLeaksTest.vue) iterates a
// list of them, wires results into its reactive `leakTest[]`, and runs the
// MaxMind geo lookup separately.
import { isValidIP } from '../valid-ip.js';
import { isValidIP } from '../ip/valid-ip.js';
export { ipApi } from './ipapi.js';
export { surfshark } from './surfshark.js';
@@ -10,7 +10,7 @@ import {
MINE_LIST_ID,
DEFAULT_LIST_MEMBERS,
faviconPath,
} from '../data/connectivity-import-lists.js';
} from '../../data/connectivity-import-lists.js';
export const CONNECTIVITY_LISTS_SCHEMA_VERSION = 1;
@@ -2,5 +2,5 @@
// record-type picker and the backend guard read the same list (same pattern as
// valid-ip.js and bgp-prefix.js).
//
// import { DNS_RECORD_TYPES } from '@/utils/dns-record-types.js';
export { DNS_RECORD_TYPES } from '../../common/dns-record-types.js';
// import { DNS_RECORD_TYPES } from '@/utils/features/dns-record-types.js';
export { DNS_RECORD_TYPES } from '../../../common/dns-record-types.js';
@@ -2,7 +2,7 @@
// Globalping probes, grouped into the five continent buckets the country
// pickers display. Served by our backend (/api/globalping-probes).
import { fetchWithTimeout } from './fetch-with-timeout.js';
import { fetchWithTimeout } from '../fetch-with-timeout.js';
let inventoryPromise = null;
@@ -15,7 +15,7 @@
import {
parseIPv4, parseIPv6, parseIp, parseCidr, formatIPv4, formatIPv6, formatIp, formatCidr,
toOctets, toHextets, cidrInfo, prefixContains, aggregateCidrs, rangeToCidrs,
} from './ip-math.js';
} from '../ip/ip-math.js';
const MAX_V4 = (1n << 32n) - 1n;
const MAX_V6 = (1n << 128n) - 1n;
+3
View File
@@ -0,0 +1,3 @@
// Thin re-export of common/mac-input.js (shared with /api/macchecker), so
// front-end code keeps importing `@/utils/features/mac-input.js`.
export { normalizeMacQuery } from '../../../common/mac-input.js';
@@ -6,7 +6,7 @@
// numeric columns, so the header row drives the column mapping and any line
// that doesn't look like a hop is skipped, never an error.
import { isValidIP } from './valid-ip.js';
import { isValidIP } from '../ip/valid-ip.js';
// Header token → hop field. Tokens not listed here (and their values) are
// dropped so the output always fits the report schema whitelist.
File renamed without changes.
@@ -21,8 +21,8 @@
// onCountryClick — optional callback, only called for countries with data
// isActive — optional guard checked after lazy loading
// }) → Promise<Chart|null> (null on load failure or a stale render)
import { NUMERIC_TO_ALPHA2 } from '../data/country-numeric.js';
import getCountryName from '../data/country-name.js';
import { NUMERIC_TO_ALPHA2 } from '../../data/country-numeric.js';
import getCountryName from '../../data/country-name.js';
let chartCtor = null;
let worldFeatures = null;
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff. Show more