Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1752576
Feat(mobile): scroll-hide nav, iOS PWA safe-area painter, theme refactor
jason5ng32 May 26, 2026
c526d46
Improvements
jason5ng32 May 26, 2026
5fcb1dd
UX Improvements
jason5ng32 May 26, 2026
0ed538f
UX Improvements
jason5ng32 May 26, 2026
5a4324f
Bug fixed
jason5ng32 May 27, 2026
7d5ee89
Bug fixed
jason5ng32 May 27, 2026
8ee3d6f
Improvements
jason5ng32 May 27, 2026
72bb07e
UI Improvements
jason5ng32 May 27, 2026
8fdd208
Improvements
jason5ng32 May 27, 2026
f728693
UI Improvements
jason5ng32 May 27, 2026
fccd320
UI Improvements
jason5ng32 May 27, 2026
4a1b3b8
Improvements
jason5ng32 May 27, 2026
e4bf75b
Improvements
jason5ng32 May 27, 2026
047b30a
Improvements
jason5ng32 May 27, 2026
d3a2412
Improvements
jason5ng32 May 27, 2026
dd44314
Improvements
jason5ng32 May 27, 2026
fd67808
Improvements
jason5ng32 May 27, 2026
c400058
Improvements
jason5ng32 May 27, 2026
a4bf032
Improvements
jason5ng32 May 27, 2026
f41c16f
Improvements
jason5ng32 May 27, 2026
4cc447e
Improvements
jason5ng32 May 27, 2026
e83d91a
Improvements
jason5ng32 May 27, 2026
5a5c0b6
Improvements
jason5ng32 May 27, 2026
d016278
Improvements
jason5ng32 May 27, 2026
2fc5fe5
Improvements
jason5ng32 May 27, 2026
4e4ac74
Refactor(system-detect): harden UA sniffing and tighten comments
jason5ng32 May 27, 2026
a238b4a
Improvements
jason5ng32 May 27, 2026
444992e
Improvements
jason5ng32 May 27, 2026
b6fba97
Improvements
jason5ng32 May 28, 2026
79b7ac6
Chore(cleanup): remove dead imports, exports, and unused preference key
jason5ng32 May 30, 2026
8b6ee36
Style: remove dead legacy CSS classes
jason5ng32 May 30, 2026
e5492e6
Chore(i18n): drop dead locale keys across en/zh/fr/tr
jason5ng32 May 30, 2026
9dcb152
Chore(ui): remove unused button-group primitive
jason5ng32 May 30, 2026
bbed82a
Chore(ui): remove unused input-group primitive
jason5ng32 May 30, 2026
94ffddc
Refactor(advanced-tools): share Globalping default locations and sele…
jason5ng32 May 30, 2026
76b7489
Refactor(status-tone): extract shared isFieldPending helper
jason5ng32 May 30, 2026
3774b83
Refactor(api): extract resolveAsnOrgName into common/ripestat.js
jason5ng32 May 30, 2026
c9e22e3
Refactor(api): share SUPPORTED_LANGS across handlers
jason5ng32 May 30, 2026
94a0676
Refactor(api): extract makeGeoHandler factory for geo source handlers
jason5ng32 May 30, 2026
4f6c3b1
Fix(i18n): correct invisibilitytest score key names in fr locale
jason5ng32 May 30, 2026
bccd0c5
Fix(i18n): add missing invisibilitytest latencyVSPing keys to en/fr/tr
jason5ng32 May 30, 2026
96036f2
Fix(i18n): add missing Turkish translation for whois.Provider
jason5ng32 May 30, 2026
b3154b5
Refactor(frontend): move use-speedtest-charts to composables
jason5ng32 May 30, 2026
e8e2b55
Refactor(frontend): move scroll-to from composables to utils
jason5ng32 May 30, 2026
3eac8b7
Refactor(frontend): rename use-analytics to analytics
jason5ng32 May 30, 2026
7483b75
Docs(frontend): define lib/composables/utils directory roles
jason5ng32 May 30, 2026
19ae242
Refactor(ui): consolidate floating buttons into FloatingDock container
jason5ng32 May 30, 2026
511d647
Improvements
jason5ng32 May 30, 2026
be6eb2e
Improvements
jason5ng32 May 30, 2026
1d09e2b
Improvements
jason5ng32 May 31, 2026
be1b9ef
Update Readme
jason5ng32 May 31, 2026
23d77ac
Merge pull request #335 from jason5ng32/dev
jason5ng32 May 31, 2026
74d2023
Improvements
jason5ng32 May 31, 2026
b75ca32
Merge pull request #336 from jason5ng32/dev
jason5ng32 May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@ npm-debug.log*
yarn-debug.log*
pnpm-debug.log*

# Runtime-downloaded databases — never bake them into the image. A clean CI build
# wouldn't have them anyway; this keeps a local build (which has them on disk from
# `pnpm dev`) consistent with CI, so the container always downloads fresh data on
# first boot. Kept in sync with the matching block in .gitignore.
common/maxmind-db/.maxmind-update-*.json
common/maxmind-db/.maxmind-update.lock
common/maxmind-db/*.bak
common/maxmind-db/*.next
common/maxmind-db/*.mmdb

common/as-org-db/*.txt
common/as-rel-db/*.txt
common/as-org-db/.caida-update-state.json
common/as-rel-db/.caida-update-state.json
common/as-org-db/.caida-update.lock
common/as-rel-db/.caida-update.lock
common/as-org-db/*.bak
common/as-rel-db/*.bak
common/as-org-db/*.next
common/as-rel-db/*.next
20 changes: 13 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,26 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Setup pnpm
# Reads the `packageManager` field in package.json to pin the pnpm version,
# so CI uses the exact same pnpm as local dev.
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
# Intentionally no `cache: npm` — that requires a package-lock.json, and this
# repo gitignores the lockfile by design. `npm install` below runs cold every
# run; dependency install on this project takes ~30s, which is acceptable
# versus the drift risk of committing a lockfile we don't otherwise use.
# The committed pnpm-lock.yaml lets setup-node cache the pnpm store keyed
# off the lockfile hash — installs are warm on every run after the first.
cache: pnpm

- name: Install dependencies
run: npm install --no-audit --no-fund
# --frozen-lockfile: install strictly from the lockfile and fail if it is
# out of sync with package.json, rather than silently rewriting it.
run: pnpm install --frozen-lockfile

- name: Run tests
run: npm test
run: pnpm test

- name: Build
run: npm run build
run: pnpm run build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public/llms.txt
public/llms-full.txt
public/tools/
public/robots.txt
public/ads.txt

# Private AI context (per-machine, not for the public repo)
local-context.md
22 changes: 12 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se

| Command | What it does |
|---|---|
| `npm run dev` | Vite + backend (nodemon) together — front-end 5173, back-end 11966 |
| `npm run build` | Front-end production build |
| `npm run preview` | Vite preview of the build output |
| `npm run start` | Built front-end + backend (static file server) |
| `npm test` | Run all `tests/*.test.js` specs |
| `npm run check` | `test` + `build`, the pre-commit self-check |
| `pnpm dev` | Vite + backend (nodemon) together — front-end 5173, back-end 11966 |
| `pnpm build` | Front-end production build |
| `pnpm preview` | Vite preview of the build output |
| `pnpm start` | Built front-end + backend (static file server) |
| `pnpm test` | Run all `tests/*.test.js` specs |
| `pnpm check` | `test` + `build`, the pre-commit self-check |

This project uses **pnpm** as its package manager (pinned via the `packageManager` field in `package.json`). The lockfile is `pnpm-lock.yaml` (committed); `pnpm-workspace.yaml` holds the `allowBuilds` approvals for the few dependencies whose install scripts are trusted to run. Do not use `npm` / `yarn` — they would produce a competing lockfile.

## Project layout

Expand All @@ -61,7 +63,7 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
├── tests/ ← Node test runner specs
├── backend-server.js ← Express app (default port 11966)
├── frontend-server.js ← static file server for `npm start`
├── frontend-server.js ← static file server for `pnpm start`
├── index.html ← Vite entry; #app mounts vaul-drawer-wrapper
├── vite.config.js
├── jsconfig.json ← JS project, alias @ → frontend/
Expand Down Expand Up @@ -96,10 +98,10 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
## Testing

- **Test runner:** Node built-in (`node --test`), no third-party framework. Specs live in `tests/*.test.js`.
- **Coverage expectation:** any non-visual logic that can be exercised without a network call — pure functions, composables with mockable inputs, transform utilities, validators — ships with a test in `tests/` and is wired into `npm test`. UI rendering, real network behavior, and browser-API-dependent code are out of scope.
- **Coverage expectation:** any non-visual logic that can be exercised without a network call — pure functions, composables with mockable inputs, transform utilities, validators — ships with a test in `tests/` and is wired into `pnpm test`. UI rendering, real network behavior, and browser-API-dependent code are out of scope.
- **Big new features:** write the tests in the same change. Don't defer.
- **Modifying a tested feature:** check the related tests; update them in the same change if behavior shifts.
- **Tests must pass locally before you hand off.** If `npm run check` is red, don't ask the user to review.
- **Tests must pass locally before you hand off.** If `pnpm check` is red, don't ask the user to review.

## Security & Boundaries

Expand All @@ -114,7 +116,7 @@ The backend enforces access control and timeouts through shared middleware rathe
- **Branch discipline — `dev` in, `dev` out.** All work starts from `dev` and lands on `dev`. `main` is only updated via PRs that merge `dev` → `main`; never base a branch on `main`, never push directly to `main`. When an AI assistant operates from a worktree and needs to fast-forward `dev`, use `git push . HEAD:dev` (the repo has `receive.denyCurrentBranch=updateInstead` set, so git syncs the main worktree's files too when it's clean) rather than `git update-ref`, which leaves the main worktree's files out of sync with HEAD.
- **Do not commit without explicit user approval.** The flow is: AI edits → user reviews → user tests → user says "commit" → AI commits. Silent commits are a breach of trust.
- **One concern per commit.** Don't mix unrelated changes into a single commit. Split at the right seam.
- **Self-test before handing off.** Run `npm run check` (or at least `npm test`) for every change. If the change is visual (UI layout, styling, interactions) and can't be verified headless, say so explicitly so the user can test it in `npm run dev`.
- **Self-test before handing off.** Run `pnpm check` (or at least `pnpm test`) for every change. If the change is visual (UI layout, styling, interactions) and can't be verified headless, say so explicitly so the user can test it in `pnpm dev`.
- **Every commit is gated on user testing.** Even with tests green, a visual change needs the user to have looked at it before it lands.
- **Add yourself as a co-author to the commit.** If you are an AI.
- **Commit message style** follows recent `git log` — `Refactor(xxx): …` / `Fix(ui): …` / `Feat(xxx): …` / `Style: …` / `Chore: …` prefix.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ To set up the development environment for this project, you'll need to:

1. Install Node.js, Vite, and Vue3.
2. Clone the repository.
3. Run `npm install` to install dependencies.
3. Run `pnpm install` to install dependencies.
4. Follow the instructions for Docker and Vercel deployment in our documentation if necessary.

### Testing

Ensure that all tests pass and, if applicable, add new tests for your changes. Run `npm test` to execute tests.
Ensure that all tests pass and, if applicable, add new tests for your changes. Run `pnpm test` to execute tests.

### Pull Request Guidelines

Expand Down
14 changes: 11 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# Build stage
FROM node:24-alpine AS build-stage
# corepack ships with the node image and provisions pnpm at the version pinned
# by the `packageManager` field in package.json — no global npm install needed.
RUN corepack enable
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copy the manifests first so this layer (and the install below) stays cached
# unless dependencies actually change. pnpm-workspace.yaml carries the
# allowBuilds approvals; pnpm-lock.yaml is required by --frozen-lockfile.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN npm run build
RUN pnpm run build

# Production stage
FROM node:24-alpine AS production-stage
WORKDIR /app
# node_modules is copied as-is from the build stage (pnpm's symlink farm into
# .pnpm is preserved within /app), so the runtime needs no install step.
COPY --from=build-stage /app/node_modules ./node_modules
COPY --from=build-stage /app/package.json ./
COPY --from=build-stage /app/dist ./dist
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,17 @@ Clone the code:
git clone https://github.com/jason5ng32/MyIP.git
```

Install and build:
Install and build. This project uses pnpm — if you don't have it, install it first (npm ships with Node, so this command always works):

```bash
npm install && npm run build
npm install -g pnpm
pnpm install && pnpm run build
```

Run:

```bash
npm start
pnpm start
```

The program will run on port 18966.
Expand Down
7 changes: 4 additions & 3 deletions README_FR.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,17 @@ Clonez le code :
git clone https://github.com/jason5ng32/MyIP.git
```

Installer & Construire :
Installer & Construire. Ce projet utilise pnpm — si vous ne l'avez pas, installez-le d'abord (npm est fourni avec Node, donc cette commande fonctionne toujours) :

```bash
npm install && npm run build
npm install -g pnpm
pnpm install && pnpm run build
```

Exécuter:

```bash
npm start
pnpm start
```

Le programme s'exécutera sur le port 18966.
Expand Down
7 changes: 4 additions & 3 deletions README_TR.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,17 @@ Kodu klonlayın:
git clone https://github.com/jason5ng32/MyIP.git
```

Kurun ve derleyin:
Kurun ve derleyin. Bu proje pnpm kullanır — eğer yoksa önce onu kurun (npm, Node ile birlikte gelir, bu yüzden bu komut her zaman çalışır):

```bash
npm install && npm run build
npm install -g pnpm
pnpm install && pnpm run build
```

Çalıştırın:

```bash
npm start
pnpm start
```

Uygulama 18966 portunda çalışacaktır.
Expand Down
7 changes: 4 additions & 3 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,17 @@
git clone https://github.com/jason5ng32/MyIP.git
```

安装与编译:
安装与编译。本项目使用 pnpm,如果你还没有,请先安装(npm 随 Node 一起提供,这条命令一定能用):

```bash
npm install && npm run build
npm install -g pnpm
pnpm install && pnpm run build
```

运行:

```bash
npm start
pnpm start
```

程序会运行在 18966 端口。
Expand Down
21 changes: 5 additions & 16 deletions api/asn-connectivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
// 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.

import { fetchAsOverview, extractOrgFromHolder } from '../common/ripestat.js';
import { lookupAsOrgName } from '../common/as-org-db.js';
import { resolveAsnOrgName } from '../common/ripestat.js';
import { providersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js';
import logger from '../common/logger.js';

Expand All @@ -21,20 +20,10 @@ const MAX_DEPTH = 3;
// as a proxy for "primary transit".
const MAX_INTERMEDIATE_BRANCH = 3;

// Two-tier org name resolver: local CAIDA as2org first (µs), RIPEstat
// as-overview fallback when the snapshot doesn't have the ASN.
async function resolveOrgName(asn) {
const local = lookupAsOrgName(asn);
if (local) return local;
try {
const res = await fetchAsOverview(asn);
if (!res.ok) return null;
const payload = await res.json();
return extractOrgFromHolder(payload?.data?.holder);
} catch {
return null;
}
}
// Two-tier org name resolver lives in common/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);

async function buildGraph(origin) {
const nodes = new Map();
Expand Down
27 changes: 8 additions & 19 deletions api/asn-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@

import {
fetchRoutingHistory,
fetchAsOverview,
extractOrgFromHolder,
resolveAsnOrgName,
} from '../common/ripestat.js';
import { lookupAsOrgName } from '../common/as-org-db.js';
import logger from '../common/logger.js';

const prefixLength = (prefix) => parseInt((prefix || '').split('/')[1], 10);
Expand Down Expand Up @@ -58,22 +56,13 @@ function summarizeOrigin(entry, minLen) {
};
}

// Two-tier resolver: local CAIDA as2org first (µs), RIPEstat as-overview
// fallback. Best-effort — any failure yields null so the row drops to
// ASN-only display rather than blocking the whole batch.
async function resolveOrgName(asn) {
const local = lookupAsOrgName(asn);
if (local) return local;
try {
const res = await fetchAsOverview(asn);
if (!res.ok) return null;
const payload = await res.json();
return extractOrgFromHolder(payload?.data?.holder);
} catch (error) {
logger.warn({ err: error, asn }, 'as-overview lookup failed');
return null;
}
}
// Two-tier resolver lives in common/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) =>
resolveAsnOrgName(asn, {
onError: (error) => logger.warn({ err: error, asn }, 'as-overview lookup failed'),
});

export default async (req, res) => {
// Prefix presence + validity guaranteed by requireValidPrefix middleware.
Expand Down
4 changes: 2 additions & 2 deletions api/dns-leak-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
import { pickLang } from '../common/langs.js';

const TOKEN_RE = /^[0-9a-f]{32}$/;
const SUPPORTED_LANGS = ['zh-CN', 'en', 'fr', 'tr'];

export async function getSessionResult(req, res) {
if (req.method !== 'GET') {
Expand All @@ -30,7 +30,7 @@ export async function getSessionResult(req, res) {
return res.status(500).json({ error: 'API key is missing' });
}

const lang = SUPPORTED_LANGS.includes(req.query.lang) ? req.query.lang : 'zh-CN';
const lang = pickLang(req.query.lang, 'zh-CN');

const url = new URL(`${apiEndpoint}/dnsleaktest/session/${token}`);
url.searchParams.set('apikey', apiKey);
Expand Down
3 changes: 1 addition & 2 deletions api/dns-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ const dnsServers = {
'Quad9': '9.9.9.9',
'ControlD': '76.76.2.0',
'AdGuard': '94.140.14.14',
'Quad 101': '101.101.101.101',
'AliDNS': '223.5.5.5',
'DNSPod': '119.29.29.29',
'114DNS': '114.114.114.114',
'China Unicom': '123.123.123.123',
'DNS4EU': '86.54.11.1',
};

// DNS-over-HTTPS server list
Expand Down
28 changes: 11 additions & 17 deletions api/ip-sb.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';
// /api/ipsb — geolocation source handler backed by api.ip.sb.
// Token-free upstream; normalizes the response into the canonical geo
// shape via the shared makeGeoHandler factory.

export default async (req, res) => {
// IP presence + validity guaranteed by requireValidIP middleware.
const ipAddress = req.query.ip;

const url = `https://api.ip.sb/geoip/${ipAddress}`;
import { makeGeoHandler } from '../common/geo-handler.js';

try {
const apiRes = await fetchUpstream(url);
const json = await apiRes.json();
res.json(modifyJsonForIPSB(json));
} catch (e) {
logger.error({ err: e, ip: ipAddress }, 'ip-sb handler failed');
res.status(500).json({ error: e.message });
}
};
function buildUrl(req) {
const ipAddress = req.query.ip;
return `https://api.ip.sb/geoip/${ipAddress}`;
}

function modifyJsonForIPSB(json) {
return {
Expand All @@ -30,4 +22,6 @@ function modifyJsonForIPSB(json) {
asn: "AS" + json.asn,
org: json.isp
};
}
}

export default makeGeoHandler({ name: 'ip-sb', buildUrl, normalize: modifyJsonForIPSB });
Loading
Loading