diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 105a2c0d7..249fb7f52 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,58 +1,33 @@ --- name: Bug report -about: Bug Report Template -title: "[Short Description of the Bug]" +about: Something isn't working +title: '' labels: bug assignees: '' --- -## Environment: +## What happened? -* Operating System: -* Browser (if applicable): -* Node Version: -* Vite Version: -* Vue3 Version: -* Docker Version (if applicable): -* Deployment Method (Vercel/Docker/Node): -* Environment Variables (without sensitive info): -* Any other relevant software versions: +A clear description of the bug. Screenshots or recordings help a lot. -## Description: +## Steps to reproduce -Provide a clear and concise description of what the bug is. +1. … +2. … -Steps to Reproduce: +## What did you expect? -1. Step one to reproduce -2. Step two... -3. ... +## Environment -## Expected Behavior: +- Where: ipcheck.ing / self-hosted (Docker / Node / Vercel) +- OS & browser (for page issues): +- Node version (self-hosted only): -Describe what you expected to happen. +## Logs (if applicable) -## Actual Behavior: +Browser console errors for page issues; terminal output for self-hosted / backend issues. -Describe what actually happened. Include screenshots or screen recordings if possible. - -## Terminal & Console Logs: - -**Important**: Please include the terminal logs and browser console logs if applicable. These logs are crucial for identifying the root cause of the issue. - -### Terminal Logs (include any error messages or stack traces): - -[Logs here] - -### Browser Console Logs (if frontend issue): - -[Logs here] - -Additional Context: - -Add any other context about the problem here, like links to related issues, logs, etc. - -## Possible Solution (optional): - -If you have an idea of how to fix the issue, please describe it here. +```text +paste logs here +``` diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 16c0188a9..342a7c0f2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,13 @@ -# Pull Request Template +> ⚠️ **Open this PR against the `dev` branch, not `main`.** `main` only receives release merges from `dev`. -> ⚠️ **Please open this PR against the `dev` branch, not `main`.** All contributions go through `dev`; `main` only receives release merges. +## What & why -## Description -Please include a summary of the change and which issue is fixed. +Summary of the change, and the issue it fixes if any (e.g. `Closes #123`). -## Type of change -- [ ] Bug fix -- [ ] New feature -- [ ] Breaking change -- [ ] Documentation update +## Checklist -## Checklist: -- [ ] This PR targets the `dev` branch, not `main`. -- [ ] I have followed the contribution guidelines. -- [ ] My code follows the style guidelines of this project. -- [ ] I have performed a self-review of my own code. -- [ ] I have commented my code, particularly in hard-to-understand areas. -- [ ] I have made corresponding changes to the documentation. -- [ ] My changes generate no new warnings. -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] New and existing unit tests pass locally with my changes. +- [ ] Targets `dev`; one concern per PR +- [ ] `pnpm check` is green locally (tests + build) +- [ ] Logic changes ship with a spec in `tests/` +- [ ] User-visible copy lands in all four locales (`en` / `zh` / `fr` / `ru`) +- [ ] I've read [CONTRIBUTING.md](../CONTRIBUTING.md) and the relevant `AGENTS.md` diff --git a/.gitignore b/.gitignore index a3b6e7f3b..d19780a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,8 +54,13 @@ common/as-rel-db/*.next docs/ .plan/ -# Local Scripts -scripts/ +# Local Scripts (fetch-favicons.js is the one public exception — +# contributors need it for the Connectivity favicon pipeline) +scripts/* +!scripts/fetch-favicons.js + +# Section banners — deploy-time data (ads and campaign promos stay out of git) +frontend/data/banners/* # Site configuration public/sitemap.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b82597024..f3e8ce7f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,82 +1,118 @@ -# CONTRIBUTING.md - -## Welcome Contributors! 👋 - -We're delighted that you're interested in contributing to our project! This document provides guidelines to ensure a smooth contribution process for everyone involved. - -### Table of Contents - -- [CONTRIBUTING.md](#contributingmd) - - [Welcome Contributors! 👋](#welcome-contributors-) - - [Table of Contents](#table-of-contents) - - [Code of Conduct](#code-of-conduct) - - [Getting Started](#getting-started) - - [Reporting Bugs](#reporting-bugs) - - [Feature Requests](#feature-requests) - - [Submitting Changes](#submitting-changes) - - [Setting Up Your Environment](#setting-up-your-environment) - - [Testing](#testing) - - [Pull Request Guidelines](#pull-request-guidelines) - - [Code Review Process](#code-review-process) - - [Community and Support](#community-and-support) - - [Thank You! 👏](#thank-you-) - -### Code of Conduct - -This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. - -### Getting Started - -If you're new to the project, consider: - -- Reading the project's README for an overview. -- Checking the issues labeled as `good first issue` for an easy entry point. - -### Reporting Bugs - -Please follow the bug report template provided in [BUG_REPORT_TEMPLATE.md](.github/ISSUE_TEMPLATE/bug_report.md). Include terminal and console logs for a comprehensive report. - -### Feature Requests - -We love to hear your ideas! Open an issue with the tag `feature request` and provide a clear and detailed explanation of the feature and its benefits. - -### Submitting Changes - -Before making any changes, please: - -1. Open a new issue discussing your proposed change. -2. Fork the repository and create your branch from **`dev`** — all contributions are based on `dev`, not `main`. - -### Setting Up Your Environment - -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 `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 `pnpm test` to execute tests. - -### Pull Request Guidelines - -When you're ready to submit your changes: - -1. **Open your pull request against the `dev` branch — not `main`.** `main` only receives release merges from `dev`, so any PR targeting `main` will be asked to retarget. -2. Rebase your branch onto the latest `dev` before submitting. -3. Keep one concern per PR — don't bundle unrelated changes (e.g. a feature plus a dev-environment tweak) into the same pull request. -4. Ensure your changes adhere to the coding standards and guidelines, with a clear description of what you changed. - -### Code Review Process - -The project maintainers will review your pull request. They might request changes or provide feedback before merging. - -### Community and Support - -Join our community channels (link your channels here) for support and discussions. - -## Thank You! 👏 - -Your contributions are what make this community great. We appreciate your efforts in making this project better! +# Contributing to MyIP + +Thanks for your interest in contributing! MyIP ([IPCheck.ing](https://ipcheck.ing)) is an +open-source IP toolbox — IP lookup, connectivity tests, WebRTC / DNS-leak detection, +speed test, and more — built as a Vue 3 SPA with an Express 5 backend. + +New here? Look for issues labeled +[`good first issue`](https://github.com/jason5ng32/MyIP/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +— they're scoped to be doable without knowing the whole codebase. + +## Quick start + +1. **Fork** the repo and create your branch **from `dev`** (never `main` — `main` only + receives release merges from `dev`). +2. **Set up:** Node.js 20+ (CI runs on Node 24) and pnpm. The pnpm version is pinned + via the `packageManager` field, so the easiest path is: + + ```bash + corepack enable # or: npm install -g pnpm + pnpm install + pnpm dev # starts Vite + the backend together + ``` + + **pnpm only.** npm or yarn would produce a competing lockfile — PRs that touch + `package-lock.json` / `yarn.lock` will be asked to redo the install with pnpm. +3. Make your change — **one concern per PR**, don't bundle unrelated changes. +4. **Run `pnpm check`** (tests + production build). It must be green before you open a PR. +5. Open your PR **against `dev`**, rebased onto the latest `dev`, with a clear + description of what changed and why. + +For anything non-trivial, open an issue first to discuss the approach — it saves you +from building something that can't be merged. + +## Project map + +``` +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, …) +tests/ Node test runner specs (node --test) +``` + +**The real architecture and convention docs are the AGENTS.md files:** +[`AGENTS.md`](AGENTS.md) (root), [`frontend/AGENTS.md`](frontend/AGENTS.md), and +[`api/AGENTS.md`](api/AGENTS.md). Don't let the filename fool you — they're written +for humans and AI agents alike, and they're the single source of truth for how this +repo works. Read the root one plus whichever half you're touching before writing code. + +Prefer a guided tour? The online **[Developer Guide](https://docs.ipcheck.ing/developer)** +walks through the project architecture, configuration, and deployment in detail +(also available in 中文, français, and русский). + +## Key conventions (short version) + +Details and rationale live in the AGENTS.md files; the headlines: + +- **JavaScript only** — no TypeScript, no `lang="ts"`. +- **New functions use `const` arrow syntax** (`const fn = async () => {}`). +- **Every new file opens with a header comment** stating its purpose. +- **Four locales land together** — user-visible copy ships in `en` / `zh` / `fr` / `ru` + in the same PR (`frontend/locales/`; tests enforce this for some surfaces). +- **Backend logging goes through the shared pino logger** (`common/logger.js`) — + no `console.*` in `api/` or `common/`. + +## Good places to start + +**DNS resolvers** — the resolver list lives in `api/data/dns-resolvers.js`, a +country-annotated data file written for exactly this kind of PR (the header +comment documents the entry shape and rules, and `tests/dns-resolvers-data.test.js` +checks your entry). Adding a well-known public resolver — especially from a +country not yet represented — is a one-object change; the UI groups results +by country automatically. + +**Connectivity test sites & lists** — curated site lists live in +`frontend/data/connectivity-import-lists.js`. Each member needs a committed 64px PNG +icon at `public/favicons/.png`, but you normally don't source it yourself: run +`pnpm test` locally and the data test auto-downloads any missing icons (also +runnable directly as `pnpm fetch-favicons`). Only if auto-fetch can't find a usable +PNG do you hand-source one (on macOS, `sips` handles ICO→PNG). Remember to commit +the PNGs with your change — CI stays offline and only checks they exist. + +**README translations** — beyond the four READMEs we maintain (`en` / `zh` / `fr` / +`ru`), community-maintained translations in any language are welcome. Create +`README_.md` from the English [README.md](README.md) (the canonical source), +open it with a one-line "community-maintained translation — English is canonical" +note in your language, and add your language to the switcher row near the top of the +existing READMEs. Keep code blocks, URLs, and badges unchanged. Check open issues +for requested languages before starting. + +**UI translations** — improvements to the existing `en` / `zh` / `fr` / `ru` packs are +welcome. Adding a whole new UI locale is heavier than it looks (every future copy +change must land in it too), so please open an issue to discuss before starting one. + +## Bugs & feature requests + +Use the issue templates in [`.github/ISSUE_TEMPLATE/`](.github/ISSUE_TEMPLATE/) — +one for bug reports (include terminal / browser console logs) and one for feature +requests. For general questions, GitHub Issues is also the right place; there are +no chat channels. + +## Testing + +- Specs live in `tests/` and run with `pnpm test` (Node's built-in test runner). +- Non-visual logic — pure functions, composables, transforms, validators — ships with + a spec in the same PR. Tests never hit real upstreams (the one exception: the + connectivity data test may download missing favicons on local runs, never in CI). +- UI rendering and browser APIs are out of scope for the Node runner; visual changes + are verified by the maintainer during review, so mention in your PR what to look at. + +## Code of Conduct + +This project follows a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, +you agree to uphold it. + +--- + +Thank you for making MyIP better! Every contribution — a one-line fix, a new +resolver, a better translation — is appreciated. diff --git a/README.md b/README.md index d4786513f..331f36246 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,13 @@ Full guides live in the MyIP Docs Center: **[docs.ipcheck.ing](https://docs.ipch * [Developer Guide](https://docs.ipcheck.ing/developer) — deployment, configuration, architecture, and contributing * [Knowledge Base](https://docs.ipcheck.ing/knowledge-base) — how to use every tool, step-by-step network diagnosis, and networking concepts +## 🤝 Contributing + +Contributions are welcome! We keep a curated set of beginner-friendly tasks — each with exact file paths, acceptance criteria, and tests that guide you to a green build: + +* 🏷️ [Good first issues](https://github.com/jason5ng32/MyIP/labels/good%20first%20issue) — add a DNS resolver from your country, add curated site lists, translate the README into your language, polish translations, and more +* 📄 [CONTRIBUTING.md](CONTRIBUTING.md) — setup, conventions, and how PRs flow (target the `dev` branch) + ## ⚙️ Configuration Two settings matter before anything else: diff --git a/README_FR.md b/README_FR.md index 925cd9de3..9c9a75dd7 100644 --- a/README_FR.md +++ b/README_FR.md @@ -99,6 +99,13 @@ Les guides complets se trouvent dans le centre de documentation MyIP : **[docs.i * [Guide du développeur](https://docs.ipcheck.ing/developer/fr) — déploiement, configuration, architecture et contribution * [Base de connaissances](https://docs.ipcheck.ing/knowledge-base/fr) — utilisation de chaque outil, diagnostic réseau pas à pas, concepts réseau +## 🤝 Contribuer + +Les contributions sont les bienvenues ! Nous maintenons une sélection de tâches adaptées aux débutants — chacune avec les fichiers précis, les critères d'acceptation et des tests pour vous guider : + +* 🏷️ [Good first issues](https://github.com/jason5ng32/MyIP/labels/good%20first%20issue) — ajouter un résolveur DNS de votre pays, enrichir les listes de sites, traduire le README dans votre langue, améliorer les traductions… +* 📄 [CONTRIBUTING.md](CONTRIBUTING.md) — installation, conventions et flux des PR (ciblez la branche `dev`) + ## ⚙️ Configuration Deux réglages comptent avant tout : diff --git a/README_RU.md b/README_RU.md index 996baa305..cb545df19 100644 --- a/README_RU.md +++ b/README_RU.md @@ -99,6 +99,13 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest * [Руководство разработчика](https://docs.ipcheck.ing/developer/ru) — развёртывание, настройка, архитектура и участие в разработке * [База знаний](https://docs.ipcheck.ing/knowledge-base/ru) — как пользоваться каждым инструментом, пошаговая диагностика сети, сетевые концепции +## 🤝 Участие в проекте + +Мы рады вашему участию! У нас есть подборка задач для новичков — с точными путями к файлам, критериями приёмки и тестами, которые ведут вас к зелёной сборке: + +* 🏷️ [Good first issues](https://github.com/jason5ng32/MyIP/labels/good%20first%20issue) — добавьте DNS-резолвер своей страны, расширьте подборки сайтов, переведите README на свой язык, улучшите переводы +* 📄 [CONTRIBUTING.md](CONTRIBUTING.md) — настройка окружения, соглашения и процесс PR (PR направляются в ветку `dev`) + ## ⚙️ Конфигурация Прежде всего важны две настройки: diff --git a/README_ZH.md b/README_ZH.md index f347793be..be5ba2dda 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -99,6 +99,13 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest * [开发者指南](https://docs.ipcheck.ing/developer/zh) —— 部署、配置、架构说明与参与贡献 * [知识库](https://docs.ipcheck.ing/knowledge-base/zh) —— 每个工具的使用说明、网络问题排查指南、网络概念科普 +## 🤝 参与贡献 + +欢迎参与贡献!我们维护着一批对新手友好的任务,每条都写明了具体文件、验收标准,并有测试引导你完成: + +* 🏷️ [Good first issues](https://github.com/jason5ng32/MyIP/labels/good%20first%20issue) —— 添加你所在国家的 DNS 解析服务、添加精选网站列表、把 README 翻译成你的语言、润色翻译等 +* 📄 [CONTRIBUTING.md](CONTRIBUTING.md) —— 环境搭建、开发约定与 PR 流程(请将 PR 提交到 `dev` 分支) + ## ⚙️ 配置 开始之前,有两项配置最重要: diff --git a/api/AGENTS.md b/api/AGENTS.md index 056b44391..ae3d62878 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -21,6 +21,10 @@ proxies (`get-user-info` / `update-user-achievement`), platform (`configs` / `sentry-tunnel` / `share-report`). Each file's header comment states its route and purpose — read those for specifics. +`api/data/` holds contributor-editable static config consumed by handlers — +currently `dns-resolvers.js`, the country-annotated resolver list behind +`dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). + ## Conventions - **Handler shape.** Single default export `async (req, res) => …`: read diff --git a/api/data/dns-resolvers.js b/api/data/dns-resolvers.js new file mode 100644 index 000000000..2ffab8468 --- /dev/null +++ b/api/data/dns-resolvers.js @@ -0,0 +1,45 @@ +// api/data/dns-resolvers.js — the curated resolver list behind /api/dnsresolver. +// +// 👋 Contributors: this file is THE place to add a public DNS resolver. +// Add one object to DNS_RESOLVERS below; no other backend change is needed +// (the frontend groups results by `country` automatically). Entry shape: +// +// { +// id: 'google', // unique lowercase slug, [a-z0-9-] +// name: 'Google', // display name shown in the UI +// country: 'US', // ISO 3166-1 alpha-2, UPPERCASE +// // ('EU' allowed for EU-wide services) +// udp: '8.8.8.8', // optional — public UDP resolver IPv4 +// doh: 'https://dns.google/resolve?', // optional — DoH JSON endpoint prefix +// } +// +// Rules (enforced by tests/dns-resolvers-data.test.js — run `pnpm test`): +// - At least one of `udp` / `doh` per entry. +// - `doh` must be an https:// URL prefix ending in '?' or '&' — the handler +// appends `name=&type=` directly, so a prefix that already +// carries query params ends in '&' (see Cloudflare's `ct=…&`). The endpoint +// must speak the DNS-over-HTTPS JSON API (`Accept: application/dns-json`), +// not just RFC 8484 wire format. +// - `country` is where the operator is based (headquarters), not where the +// anycast nodes are. +// +// ⚠️ Keep this list curated, not exhaustive: EVERY resolver here adds one +// parallel upstream query per protocol to EVERY /api/dnsresolver request. +// Prefer well-known, stable, globally reachable services — and resolvers +// from countries not yet represented over a fifth US entry. +// +// Result ordering is stable: data-file order, and for providers with both +// protocols the udp lookup comes right before the doh one. + +export const DNS_RESOLVERS = [ + { id: 'google', name: 'Google', country: 'US', udp: '8.8.8.8', doh: 'https://dns.google/resolve?' }, + { id: 'cloudflare', name: 'Cloudflare', country: 'US', udp: '1.1.1.1', doh: 'https://cloudflare-dns.com/dns-query?ct=application/dns-json&' }, + { id: 'opendns', name: 'OpenDNS', country: 'US', udp: '208.67.222.222' }, + { id: 'quad9', name: 'Quad9', country: 'CH', udp: '9.9.9.9' }, + { id: 'controld', name: 'ControlD', country: 'CA', udp: '76.76.2.0' }, + { id: 'adguard', name: 'AdGuard', country: 'CY', udp: '94.140.14.14', doh: 'https://dns.adguard.com/resolve?' }, + { id: 'alidns', name: 'AliDNS', country: 'CN', udp: '223.5.5.5', doh: 'https://dns.alidns.com/resolve?' }, + { id: 'dnspod', name: 'DNSPod', country: 'CN', udp: '119.29.29.29' }, + { id: '114dns', name: '114DNS', country: 'CN', udp: '114.114.114.114' }, + { id: 'dns4eu', name: 'DNS4EU', country: 'EU', udp: '86.54.11.1' }, +]; diff --git a/api/dns-resolver.js b/api/dns-resolver.js index 560f06b3f..7b188e87d 100644 --- a/api/dns-resolver.js +++ b/api/dns-resolver.js @@ -1,8 +1,11 @@ -// api/dnsresolver.js +// api/dns-resolver.js — GET /api/dnsresolver: resolve a hostname against every +// resolver in api/data/dns-resolvers.js (UDP DNS + DoH) in parallel and return +// one flat, country-annotated result list the frontend groups by country. import { Resolver } from 'dns'; import { promisify } from 'util'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; import logger from '../common/logger.js'; +import { DNS_RESOLVERS } from './data/dns-resolvers.js'; // Bound each upstream lookup so the slowest server doesn't pin the // overall response. 3s for UDP DNS (`Resolver` rejects on first @@ -11,28 +14,8 @@ import logger from '../common/logger.js'; const DNS_TIMEOUT_MS = 3000; const DOH_TIMEOUT_MS = 5000; -// Normal DNS server list -const dnsServers = { - 'Google': '8.8.8.8', - 'Cloudflare': '1.1.1.1', - 'OpenDNS': '208.67.222.222', - 'Quad9': '9.9.9.9', - 'ControlD': '76.76.2.0', - 'AdGuard': '94.140.14.14', - 'AliDNS': '223.5.5.5', - 'DNSPod': '119.29.29.29', - '114DNS': '114.114.114.114', - 'DNS4EU': '86.54.11.1', -}; - -// DNS-over-HTTPS server list -const dohServers = { - 'Google': 'https://dns.google/resolve?', - 'Cloudflare': 'https://cloudflare-dns.com/dns-query?ct=application/dns-json&', - 'AdGuard': 'https://dns.adguard.com/resolve?', - 'AliDNS': 'https://dns.alidns.com/resolve?', -}; - +// Resolve via classic UDP DNS. Returns the raw result value: an array of +// strings, a joined MX string, or 'N/A' on empty/failure. const resolveDns = async (hostname, type, name, server) => { const resolver = new Resolver({ timeout: DNS_TIMEOUT_MS, tries: 1 }); resolver.setServers([server]); @@ -74,19 +57,21 @@ const resolveDns = async (hostname, type, name, server) => { } if (addresses.length === 0 || addresses === '' || addresses === null) { - return { [name]: `N/A` }; + return 'N/A'; } - return { [name]: addresses }; + return addresses; } catch (error) { // Per-server timeouts are expected (some DNS hosts are unreachable // from a given network); demote to debug so they don't spam the // terminal during normal operation. logger.debug({ err: error, server: name }, 'DNS resolver: lookup failed, returning N/A'); - return { [name]: `N/A` }; + return 'N/A'; } }; +// Resolve via the DNS-over-HTTPS JSON API. Same return semantics as +// resolveDns. `url` is a prefix ending in '?' or '&' (see the data file). const resolveDoh = async (hostname, type, name, url) => { try { const response = await fetchUpstream(`${url}name=${hostname}&type=${type}`, { @@ -96,12 +81,12 @@ const resolveDoh = async (hostname, type, name, url) => { const data = await response.json(); const addresses = data.Answer ? data.Answer.map(answer => answer.data) : ['N/A']; if (addresses.length === 0 || addresses === '' || addresses === null) { - return { [name]: `N/A` }; + return 'N/A'; } - return { [name]: addresses }; + return addresses; } catch (error) { logger.debug({ err: error, server: name }, 'DoH resolver: lookup failed, returning N/A'); - return { [name]: `N/A` }; + return 'N/A'; } }; @@ -127,20 +112,36 @@ const dnsResolver = async (req, res) => { return res.status(400).send({ error: 'Invalid hostname' }); } - const dnsPromises = Object.entries(dnsServers).map(([name, ip]) => resolveDns(hostname, type, name, ip)); - const dohPromises = Object.entries(dohServers).map(([name, url]) => resolveDoh(hostname, type, name, url)); + // One lookup task per entry × protocol, in stable order: data-file order, + // udp before doh within a provider. Each task resolves to one row of the + // response; failures collapse to result 'N/A' inside the resolvers, so + // Promise.all never rejects here. + const lookups = DNS_RESOLVERS.flatMap((server) => { + const tasks = []; + if (server.udp) { + tasks.push(resolveDns(hostname, type, server.name, server.udp).then((result) => ({ + id: server.id, + provider: server.name, + country: server.country, + type: 'udp', + result, + }))); + } + if (server.doh) { + tasks.push(resolveDoh(hostname, type, server.name, server.doh).then((result) => ({ + id: server.id, + provider: server.name, + country: server.country, + type: 'doh', + result, + }))); + } + return tasks; + }); try { - // Execute all DNS and DoH queries in parallel - - const result_dns = await Promise.all(dnsPromises); - const result_doh = await Promise.all(dohPromises); - - res.json({ - hostname, - result_dns, - result_doh - }); + const results = await Promise.all(lookups); + res.json({ hostname, results }); } catch (error) { res.status(500).send({ error: error.message }); } diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index dfdb21e77..b0b9f8890 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -6,214 +6,145 @@ Conventions specific to the Vue 3 SPA under `frontend/`. Universal rules ## Overview Vue 3 ` diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 573c1e694..cf026be13 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -83,29 +83,27 @@ - - + + diff --git a/frontend/components/Home.vue b/frontend/components/Home.vue index 106501419..43c188ca8 100644 --- a/frontend/components/Home.vue +++ b/frontend/components/Home.vue @@ -5,11 +5,11 @@
- - - - - + + + + +
@@ -75,18 +75,14 @@ const store = useMainStore(); const configs = computed(() => store.configs); const userPreferences = computed(() => store.userPreferences); -// Template refs +// Template refs — UI chrome only; the test sections are reached through the +// command bus (utils/app-commands.js), not refs. const userRef = ref(null); const achievementsRef = ref(null); const queryIPRef = ref(null); const helpModalRef = ref(null); const shareReportRef = ref(null); -const speedTestRef = ref(null); const advancedToolsRef = ref(null); -const IPCheckRef = ref(null); -const connectivityRef = ref(null); -const webRTCRef = ref(null); -const dnsLeaksRef = ref(null); // Info mask const { infoMaskLevel, isInfosLoaded, showMaskButton, toggleInfoMask } = useInfoMask({ @@ -96,7 +92,6 @@ const { infoMaskLevel, isInfosLoaded, showMaskButton, toggleInfoMask } = useInfo // Refresh / initial load sequence const { loadingControl } = useRefreshOrchestrator({ - refs: { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef }, store, t, userPreferences, @@ -106,8 +101,7 @@ const { loadingControl } = useRefreshOrchestrator({ // Shortcuts const { loadShortcuts } = useShortcuts({ refs: { - queryIPRef, helpModalRef, shareReportRef, - speedTestRef, advancedToolsRef, IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef, + queryIPRef, helpModalRef, shareReportRef, advancedToolsRef, isInfosLoaded, toggleInfoMask, }, store, t, configs, userPreferences, diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index 25267ebb7..52f7656ce 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -22,6 +22,10 @@ :asnConnectivityInfos="asnConnectivityInfos" @refresh-card="refreshCard" /> + + + @@ -34,9 +38,11 @@ import { trackEvent } from '@/utils/analytics'; import { isUsablePublicIP } from '@/utils/valid-ip.js'; import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips'; -import { emitAppEvent } from '@/utils/app-events'; +import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; +import { useAppCommand } from '@/composables/use-app-command.js'; import { authenticatedFetch, fetchErrorLabel } from '@/utils/authenticated-fetch'; import IPCard from './ip-infos/IPCard.vue'; +import InfoBanner from './widgets/InfoBanner.vue'; const { t } = useI18n(); @@ -122,6 +128,8 @@ const IPArray = ref([]); const ipGeoSource = ref(userPreferences.value.ipGeoSource); const usingSource = ref(userPreferences.value.ipGeoSource); const fetchStatus = reactive([]); +// Timing gate for the sponsor slot: flips once every visible card settled. +const cardsHaveSettled = ref(false); // Middleware let pendingIPDetailsRequests = new Map(); @@ -232,6 +240,7 @@ const trackFetchStatus = (status) => { } } if (allHasFetched) { + cardsHaveSettled.value = true; store.setLoadingStatus('IPInfo', true); // Domain event: full snapshot of the visible cards, re-emitted whenever a // card settles after this point (single-card refresh included). The report @@ -263,6 +272,10 @@ const trackFetchStatus = (status) => { // allSettled so one card can't sink the batch (fetchIP already swallows per-card // errors — this is belt-and-suspenders). Cards paint independently as they land. const checkAllIPs = async () => { + // A whole-grid pass means no card is settled: clear the completion flags so + // ipinfo:finished (and the ipinfo:refresh command riding on it) waits for + // every card to land again, instead of re-firing on the first one. + fetchStatus.splice(0); const ipSources = [ [0, getIPFromIPChecking4], [1, getIPFromIPChecking6], @@ -430,14 +443,21 @@ watch(IPArray, () => { store.updateAllIPs(IPArray.value); }); -onMounted(() => { - store.setMountingStatus('IPInfo', true); +// Command owner: refresh one card ({ index }) or the whole grid. Resolves +// with the next ipinfo:finished snapshot — the grid re-emits it whenever a +// card settles, single-card refreshes included. +useAppCommand('ipinfo:refresh', ({ index } = {}) => { + const finished = waitForAppEvent('ipinfo:finished'); + if (Number.isInteger(index) && ipDataCards[index]) { + refreshCard(ipDataCards[index], index); + } else { + checkAllIPs(); + } + return finished; }); -defineExpose({ - checkAllIPs, - ipDataCards, - refreshCard, +onMounted(() => { + store.setMountingStatus('IPInfo', true); }); diff --git a/frontend/components/SpeedTest.vue b/frontend/components/SpeedTest.vue index 26549c6fe..d17b413e7 100644 --- a/frontend/components/SpeedTest.vue +++ b/frontend/components/SpeedTest.vue @@ -165,15 +165,19 @@ + + +