mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Docs: split AGENTS.md into root + frontend + api; slim CLAUDE.md
CLAUDE.md is now a 10-line pointer to AGENTS.md plus a Claude-specific extension slot; everything contributor-facing lives in AGENTS.md. Root AGENTS.md is the index — Overview, Stack, Commands, Project layout, universal Conventions (language / comments / i18n), Testing, a short Security & Boundaries summary, and Workflow. It references @frontend/AGENTS.md and @api/AGENTS.md for area-specific details. frontend/AGENTS.md collects the UI system (primitives, design tokens, status tones, canonical patterns) plus the shadcn-vue-first rule and frontend-specific project layout / conventions. This is what a UI contributor needs — API rules never get in the way. api/AGENTS.md covers the backend handler shape (fetchUpstream, error shape, response normalization), the middleware model (requireReferer globally, requireValidIP per-route, handlers must not repeat those checks), the private-API header pass-through exception, and the smoke test layout. New rules captured across the three files: - Every new file opens with a header comment stating its purpose - Large templates / functions carry block comments on each meaningful section - Big features and modifications to tested features update tests in the same change; `npm run check` must be green before handing off - One concern per commit - Visual changes get an explicit "please test in npm run dev" handoff - User testing must pass before commit - shadcn-vue-first for new UI components (local primitives → shadcn-vue library → hand-rolled Tailwind as last resort) - Every commit scans AGENTS.md for staleness and updates it in the same change if the rules shifted Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8a80724855
commit
c7ef2cb810
4 files changed
+313
-167
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# AGENTS.md
|
||||
|
||||
Single source of truth for anyone — human or AI — contributing to MyIP.
|
||||
|
||||
For area-specific details, see:
|
||||
|
||||
- Frontend (Vue 3 SPA): @frontend/AGENTS.md
|
||||
- Backend API (Express): @api/AGENTS.md
|
||||
|
||||
## Overview
|
||||
|
||||
**MyIP** (IPCheck.ing) is an open-source IP toolbox: IP lookup, connectivity tests, WebRTC / DNS-leak detection, speed test, MTR, Whois, security checklist, browser fingerprint, anonymity checks, and more.
|
||||
@@ -27,155 +34,6 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
|
||||
| Tests | Node built-in test runner (`node --test`) |
|
||||
| Runtime libs | chart.js · svgmap · @cloudflare/speedtest · maxmind · whoiser · thumbmarkjs · ua-parser-js · detect-gpu · circle-progress.vue |
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
.
|
||||
├── AGENTS.md ← this file (single source of truth)
|
||||
├── CLAUDE.md ← pointer to AGENTS.md
|
||||
├── refactor/ ← archived design notes
|
||||
│
|
||||
├── index.html ← #app mounts vaul-drawer-wrapper
|
||||
├── vite.config.js
|
||||
├── jsconfig.json ← JS project, alias @ → frontend/
|
||||
├── package.json
|
||||
│
|
||||
├── frontend/ ← Vite root
|
||||
│ ├── App.vue ← thin shell, delegates to composables
|
||||
│ ├── main.js
|
||||
│ ├── store.js ← Pinia main store
|
||||
│ ├── firebase-init.js
|
||||
│ ├── sw.js ← Serwist service worker
|
||||
│ ├── router/ ← Advanced Tools subroutes (opens Drawer)
|
||||
│ ├── locales/ ← i18n copy (en / zh / fr / tr) + security-checklist data
|
||||
│ ├── style/style.css ← Tailwind v4 entry + design tokens
|
||||
│ ├── lib/ ← cn() helper (tailwind-merge + clsx)
|
||||
│ ├── data/ ← static config (achievements / ip-databases / sections / default-preferences)
|
||||
│ ├── utils/ ← pure helpers (valid-ip / getips / transform-ip-data / shortcut impls / …)
|
||||
│ ├── composables/ ← reusable composition logic
|
||||
│ │ ├── use-info-mask.js
|
||||
│ │ ├── use-refresh-orchestrator.js
|
||||
│ │ ├── use-scroll-to.js
|
||||
│ │ ├── use-section-tracking.js
|
||||
│ │ ├── use-shortcuts.js
|
||||
│ │ └── use-status-tone.js ← shared 4-tier business-state → visual-color mapping
|
||||
│ └── components/
|
||||
│ ├── *.vue ← top-level sections (IpInfos / Connectivity / WebRTC / DnsLeaks / SpeedTest / Advanced / Footer / Nav / Achievements / User / Additional)
|
||||
│ ├── ip-infos/ ← IP-card subcomponents (IPCard / ASNInfo / DataPairBar)
|
||||
│ ├── advanced-tools/ ← 10 tool subpages (MtrTest / GlobalLatencyTest / RuleTest / DnsResolver / CensorshipCheck / Whois / MacChecker / BrowserInfo / InvisibilityTest / SecurityChecklist + Empty)
|
||||
│ ├── widgets/ ← small reusables (QueryIP / Help / Preferences / InfoMask / PWA / Toast)
|
||||
│ ├── svgicons/ ← a few inline SVGs
|
||||
│ └── ui/ ← shadcn-vue copy-in primitives (see UI system)
|
||||
│
|
||||
├── api/ ← backend API handlers
|
||||
├── common/ ← code shared between front- and back-end (valid-ip / maxmind-db / …)
|
||||
├── public/ ← static assets
|
||||
├── tests/ ← Node test runner specs
|
||||
├── backend-server.js ← Express API (default port 11966)
|
||||
└── frontend-server.js ← static file server for `npm start`
|
||||
```
|
||||
|
||||
## UI system
|
||||
|
||||
### Primitives
|
||||
|
||||
Located at `frontend/components/ui/`. 21 primitives are copied in:
|
||||
|
||||
`accordion` · `badge` · `button` · `button-group` · `card` · `collapsible` · `dialog` (with `DialogHeader`) · `drawer` (vaul-vue) · `dropdown-menu` · `input` · `input-group` · `progress` · `select` · `separator` · `sheet` · `sonner` · `spinner` · `switch` · `tabs` · `toggle-group` · `tooltip`
|
||||
|
||||
Three are project-specific, not in stock shadcn-vue:
|
||||
|
||||
- **`ButtonGroup`** — visual container for stitching buttons together
|
||||
- **`InputGroup`** — flattens `Input + Button` (or `Input + Select`) into a single bordered control via `[&>input] / [&>button]` passthrough selectors. The group itself carries border / ring / shadow. Use this whenever an input is paired with a trigger button — never hand-roll `rounded-r-none / -ml-px`
|
||||
- **`Spinner`** — lucide `Loader2` + `animate-spin` + `role="status"`
|
||||
|
||||
Rule: before authoring a `<button>`, `<input>`, popover, or list, check `frontend/components/ui/` first. Missing variants are not a reason to bypass the primitive — `:class` overrides, `as-child`, and tw-merge cover nearly every state-color need. Only drop to raw Tailwind when the shape or behavior does not exist.
|
||||
|
||||
### Design tokens
|
||||
|
||||
Tokens live at the top of `frontend/style/style.css`. On top of shadcn defaults, four business-semantic colors are defined:
|
||||
|
||||
| Token | Meaning | Light | Dark |
|
||||
|---|---|---|---|
|
||||
| `--info` | waiting / in-progress | sky-500 | sky-400 |
|
||||
| `--success` | ok-fast / success | green-500 | green-400 |
|
||||
| `--warning` | ok-slow / warning | amber-500 | amber-400 |
|
||||
| `--action` | the "run / trigger" brand color | blue-500 | blue-500 |
|
||||
|
||||
`--destructive` uses the shadcn default. Every token has a paired `-foreground`; `bg-*` consumers do not need `dark:` variants.
|
||||
|
||||
Rule: use semantic tokens (`bg-info` / `bg-success` / `bg-warning` / `bg-destructive` / `bg-action` / `bg-muted` / `bg-card` / `bg-accent` / `text-foreground` / `text-muted-foreground` / `border` / …). Do not write `dark:` dual pairs like `bg-neutral-50 dark:bg-neutral-900` — that pattern does not exist in this codebase.
|
||||
|
||||
### Button and Badge variants
|
||||
|
||||
Button adds `action` (blue-500 trigger color) to the shadcn defaults (`default / destructive / outline / secondary / ghost / link`).
|
||||
|
||||
Badge adds `success` to the defaults. Badge hover is globally disabled — Badge is a display element, not an interactive one. Put `hover:bg-muted` etc. on an outer `<a>` / `<button>` when interactivity is needed.
|
||||
|
||||
### Status tones
|
||||
|
||||
All "business state → visual color" mapping goes through `frontend/composables/use-status-tone.js`. It exposes four tones: `wait` / `ok-fast` / `ok-slow` / `fail`.
|
||||
|
||||
Rule: any new module that surfaces a status reuses these tones. Do not hand-write `switch` statements that map states to hex codes or Tailwind classes.
|
||||
|
||||
### Canonical patterns
|
||||
|
||||
**Trigger button** — `variant="action"` + `<Spinner />` + `:disabled` is the three-piece pattern used by QueryIP, MacChecker, DnsResolver, Whois, MtrTest, Ping, Censorship, Invisibility, etc.
|
||||
|
||||
```vue
|
||||
<Button variant="action" :disabled="isRunning" @click="run">
|
||||
<Spinner v-if="isRunning" />
|
||||
Run
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Input with trigger** — `InputGroup` flattens the pair. `SelectTrigger` renders as a button, so `Select` can swap in for `Input` with no extra work.
|
||||
|
||||
```vue
|
||||
<InputGroup>
|
||||
<Input v-model="q" placeholder="..." />
|
||||
<Button variant="action" :disabled="running" @click="run">
|
||||
<Spinner v-if="running" /> Run
|
||||
</Button>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
**Status card** — homepage status cards (Connectivity / WebRTC / DnsLeak / IPCard / RuleTest) use:
|
||||
|
||||
```vue
|
||||
<Card class="keyboard-shortcut-card jn-card transition-transform duration-300 ease-out hover:-translate-y-1.5">
|
||||
```
|
||||
|
||||
`jn-card` is the project marker for shadow / border / keyboard-nav outline. `keyboard-shortcut-card` is the marker that the J/K keyboard-shortcut navigation queries for.
|
||||
|
||||
**Flag** — always use the iconify circle-flags set:
|
||||
|
||||
```vue
|
||||
<Icon :icon="'circle-flags:' + code.toLowerCase()" class="size-4" />
|
||||
```
|
||||
|
||||
**Tables vs lists** — two or three columns with independent header semantics → `<table>` with `thead text-xs uppercase tracking-wide text-muted-foreground` + `tbody divide-y` + row `hover:bg-muted/50`. Mobile-friendly rows or asymmetric content with no header meaning → `<ul class="rounded-lg border bg-card divide-y">` + `<li>`.
|
||||
|
||||
**Dialog header** — use the `<DialogHeader :icon="SomeIcon" :title="..." />` primitive to get an icon, title, and DialogClose in one element, matching the Sheet / Drawer header treatment.
|
||||
|
||||
**Drawer** — reserved for the bottom drawer (Advanced Tools panel), via `vaul-vue`. Side panels (right / left) use `Sheet`. The Drawer defaults to `handleOnly: true` (only the top handle is draggable) and CSS is set so body text remains selectable.
|
||||
|
||||
**Motion** — card hover lift: `transition-transform duration-300 ease-out hover:-translate-y-1.5`. Panel fade-slide: scoped `.fade-slide-*` transitions. Loading: `<Spinner />`, not hand-rolled `animate-pulse` dot clusters.
|
||||
|
||||
## Code conventions
|
||||
|
||||
- **JavaScript only.** New files are `.js` / `.vue`; `<script setup>` has no `lang="ts"`. Do not rename `jsconfig.json` to `tsconfig.json` or otherwise introduce TypeScript.
|
||||
- **Composition API.** All components use `<script setup>`.
|
||||
- **Path alias.** `@` → `frontend/` (defined in `jsconfig.json` and `vite.config.js`).
|
||||
- **Comments are English by default.** Commit messages and planning docs can be Chinese; source-code comments stay English.
|
||||
- **i18n coverage.** Any new feature that surfaces copy must land in all four locales at `frontend/locales/{en,zh,fr,tr}.json` in the same change. No English-only or Chinese-only keys.
|
||||
|
||||
## 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.
|
||||
- **When adding such logic, write the test in the same change.** Don't defer.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
@@ -187,12 +45,67 @@ Rule: any new module that surfaces a status reuses these tones. Do not hand-writ
|
||||
| `npm test` | Run all `tests/*.test.js` specs |
|
||||
| `npm run check` | `test` + `build`, the pre-commit self-check |
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
.
|
||||
├── AGENTS.md ← this file
|
||||
├── CLAUDE.md ← Claude-specific pointer to AGENTS.md
|
||||
│
|
||||
├── frontend/ ← Vue 3 SPA (see frontend/AGENTS.md)
|
||||
├── api/ ← Express handlers (see api/AGENTS.md)
|
||||
├── common/ ← code shared between front- and back-end
|
||||
│ (valid-ip / fetch-with-timeout / guards /
|
||||
│ referer-check / maxmind-service / …)
|
||||
├── tests/ ← Node test runner specs
|
||||
│
|
||||
├── backend-server.js ← Express app (default port 11966)
|
||||
├── frontend-server.js ← static file server for `npm start`
|
||||
├── index.html ← Vite entry; #app mounts vaul-drawer-wrapper
|
||||
├── vite.config.js
|
||||
├── jsconfig.json ← JS project, alias @ → frontend/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
### Language
|
||||
|
||||
- **JavaScript only.** New files are `.js` / `.vue`; `<script setup>` has no `lang="ts"`. Do not rename `jsconfig.json` to `tsconfig.json` or otherwise introduce TypeScript.
|
||||
- **English by default** for source-code comments , commit messages and AGENTS.md files. Planning docs can be in other languages. Locale packs are obviously the exception — they contain user-facing copy in their respective language.
|
||||
|
||||
### Comments
|
||||
|
||||
- **Every new file opens with a header comment** stating its purpose. One or two lines is usually enough; enough that a reader opening the file cold understands what it is.
|
||||
- **Large templates or functions carry block comments** on each meaningful section — enough for a maintainer six months later to orient quickly. Not every line, but every region / branch / step.
|
||||
|
||||
### i18n coverage
|
||||
|
||||
- Any feature that surfaces copy must land in **all four locales** (`en` / `zh` / `fr` / `tr`) in the same change. No English-only or Chinese-only keys slipping through.
|
||||
- Same rule applies to `frontend/data/changelog.json` — every entry's `change` object must have all four languages. `tests/changelog.test.js` enforces this.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## Security & Boundaries
|
||||
|
||||
The backend enforces access control and timeouts through shared middleware rather than per-handler code. Full details live in @api/AGENTS.md; the rules that matter at the project level:
|
||||
|
||||
- `requireReferer` is mounted globally on `/api/*` — handlers must not repeat the referer check.
|
||||
- `requireValidIP()` is attached per-route where `?ip` is a required param — handlers must not repeat the IP check.
|
||||
- Every upstream HTTP call goes through `fetchUpstream` (`common/fetch-with-timeout.js`) with an 8s timeout. Never add a bare `fetch()` to an `api/` handler.
|
||||
|
||||
## Workflow
|
||||
|
||||
- **Do not commit without explicit user approval.** The flow is: AI edits → user reviews → user says "commit" → AI commits. Silent commits are a breach of trust.
|
||||
- **Commit message style** follows recent `git log` — `refactor(xxx): …` / `fix(ui): …` / `feat(ui): …` / `style: …` / `chore: …` prefix, body can be Chinese.
|
||||
- **Every commit ends with** the Co-Authored-By trailer:
|
||||
|
||||
```
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
```
|
||||
- **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`.
|
||||
- **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.
|
||||
- **On every commit, scan AGENTS.md (root + relevant sub-file) for staleness** — if the change adds a convention, renames a shared module, flips a rule, or invalidates an example, update the doc in the same commit. AGENTS.md drifting from reality is the main failure mode of this kind of document.
|
||||
@@ -1,16 +1,11 @@
|
||||
# CLAUDE.md
|
||||
|
||||
> This file is automatically read by Claude Code at session start.
|
||||
Authoritative project instructions: @AGENTS.md
|
||||
|
||||
**Read [AGENTS.md](./AGENTS.md) for everything.**
|
||||
Claude-specific additions below; on conflict, AGENTS.md wins.
|
||||
|
||||
It is the single source of truth for this project:
|
||||
## Claude-specific
|
||||
|
||||
- Tech stack (what's in / what's out — no Bootstrap, no flag-icons, no TypeScript)
|
||||
- Directory layout
|
||||
- UI infrastructure: shadcn-vue primitives in `frontend/components/ui/`, design tokens, semantic color system
|
||||
- Code conventions: JavaScript only, Composition API, `variant="action" + <Spinner />` for trigger buttons, `<InputGroup>` for input + button combos, semantic tokens over `dark:` variants, etc.
|
||||
- Commit discipline (do not commit without explicit user approval)
|
||||
- "Do not" list
|
||||
|
||||
Nothing project-specific belongs here — put it in AGENTS.md so Codex / any other agent reading AGENTS.md stays in sync.
|
||||
None at the moment. This file exists so Claude Code auto-loads AGENTS.md at
|
||||
session start; additional Claude-only guidance goes here if it ever diverges
|
||||
from what all contributors should follow.
|
||||
@@ -0,0 +1,85 @@
|
||||
# api/AGENTS.md
|
||||
|
||||
Conventions for Express 5 back-end handlers under `api/` and the shared back-end
|
||||
code under `common/`. See ../AGENTS.md for universal project rules
|
||||
(language, commits, testing expectations).
|
||||
|
||||
## Overview
|
||||
|
||||
The Express app is defined in `backend-server.js` at the repo root. Every route
|
||||
is wired there and delegated to a handler module under `api/`. Shared code
|
||||
(referer check, IP validator, fetch helper, Express middleware, MaxMind service)
|
||||
lives under `common/` and is also consumed by the frontend where it makes sense
|
||||
(e.g. `valid-ip.js`, `fetch-with-timeout.js`).
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
api/
|
||||
├── configs.js ← /api/configs — reports which env-gated features are on
|
||||
├── google-map.js ← /api/map — Google Static Maps image proxy (binary stream)
|
||||
├── ipinfo-io.js, ipapi-com.js, ipapi-is.js, ip2location-io.js, ip-sb.js,
|
||||
│ ipcheck-ing.js, maxmind.js ← IP geolocation source handlers (route per source)
|
||||
├── invisibility-test.js ← /api/invisibility — proxy to private IPCheck.ing endpoint
|
||||
├── mac-checker.js ← /api/macchecker — MAC vendor lookup
|
||||
├── get-whois.js ← /api/whois — whoiser wrapper
|
||||
├── cf-radar.js ← /api/cfradar — ASN details via Cloudflare Radar
|
||||
├── dns-resolver.js ← /api/dnsresolver — DNS + DoH parallel query
|
||||
├── get-user-info.js ← /api/getuserinfo — user-profile proxy
|
||||
└── update-user-achievement.js ← /api/updateuserachievement — user-achievement proxy
|
||||
|
||||
common/
|
||||
├── fetch-with-timeout.js ← fetchWithTimeout (5s default) + fetchUpstream (8s preset)
|
||||
├── guards.js ← requireReferer + requireValidIP Express middleware
|
||||
├── referer-check.js ← low-level referer allow-list check
|
||||
├── valid-ip.js ← IPv4 / IPv6 validator (also re-exported from frontend)
|
||||
├── maxmind-service.js ← mmdb reader + lookup
|
||||
├── maxmind-updater.js ← scheduled mmdb auto-update
|
||||
└── maxmind-db/ ← GeoLite2-ASN.mmdb + GeoLite2-City.mmdb
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Handler shape.** Each handler is a single default export, `async (req, res) => …`. It reads from `req.query` / `req.body`, calls upstream, and writes one response.
|
||||
- **Every upstream HTTP call uses `fetchUpstream`** from `common/fetch-with-timeout.js`:
|
||||
|
||||
```js
|
||||
import { fetchUpstream } from '../common/fetch-with-timeout.js';
|
||||
|
||||
const apiRes = await fetchUpstream(url);
|
||||
const json = await apiRes.json();
|
||||
res.json(transform(json));
|
||||
```
|
||||
|
||||
Default timeout is 8s. Never add a bare `fetch()` or `https.get()` — if a provider hangs, the Express connection should time out, not pin indefinitely.
|
||||
- **Error shape.** `res.status(500).json({ error: error.message })` on upstream failures; `res.status(400).json({ error: '…' })` on bad input. Be terse — the frontend doesn't display these error strings verbatim.
|
||||
- **Response shape.** IP-geolocation handlers normalize their upstream's response into the canonical shape consumed by the frontend (`ip` / `country` / `country_name` / `country_code` / `latitude` / `longitude` / `asn` / `org` / …). If you add a new source, match the existing shape.
|
||||
|
||||
## Security & Boundaries
|
||||
|
||||
### Guards live in middleware, not in handlers
|
||||
|
||||
- `requireReferer` is mounted globally on `/api/*` in `backend-server.js`. It rejects any request whose `Referer` header isn't on the `ALLOWED_DOMAINS` list (plus `localhost` always). **Handlers must not repeat the referer check.**
|
||||
- `requireValidIP()` is attached per-route to every handler that takes `?ip=`. It rejects missing or malformed IPs before the handler runs. **Handlers must not repeat the IP check** — inside the handler body, `req.query.ip` is already known to be a well-formed string.
|
||||
- If you add a new handler that needs a different-shape param guard, add the guard to `common/guards.js` and attach it in `backend-server.js` rather than open-coding the check in the handler.
|
||||
|
||||
### Private-API header pass-through (intentional exception)
|
||||
|
||||
Handlers that call our own private IPCheck.ing API (`ipcheck-ing.js`, `invisibility-test.js`, `update-user-achievement.js`, `get-user-info.js`) forward the caller's request headers to the upstream:
|
||||
|
||||
```js
|
||||
const apiResponse = await fetchUpstream(url, { headers: { ...req.headers } });
|
||||
```
|
||||
|
||||
This is deliberate — the upstream expects caller context (Accept-Language, auth tokens, etc.). Do **not** replicate this pattern for third-party upstreams; third-party handlers should send only what's explicitly needed.
|
||||
|
||||
### Defensive method gates
|
||||
|
||||
Some handlers keep a `req.method !== 'GET'` (or `PUT`) branch even though Express routes already gate the method. These exist because dedicated smoke tests assert on that branch directly against the handler. If you add or copy a handler, leave the defensive gate in place if a test covers it.
|
||||
|
||||
## Testing
|
||||
|
||||
- Smoke tests for every handler live in `tests/api-handlers.test.js`. They cover method gating, param presence / validity (beyond what middleware handles), and the "API key missing" early-return branches.
|
||||
- Never hit real upstream APIs in tests — assert on branches that `return` before the first `fetchUpstream`.
|
||||
- Middleware itself is covered by `tests/guards.test.js` (referer + IP validation). Don't duplicate those assertions at the handler level.
|
||||
- `fetchUpstream` / `fetchWithTimeout` timing + abort behavior is covered by `tests/fetch-with-timeout.test.js`.
|
||||
@@ -0,0 +1,153 @@
|
||||
# frontend/AGENTS.md
|
||||
|
||||
Conventions and patterns specific to the Vue 3 SPA under `frontend/`.
|
||||
See ../AGENTS.md for universal project rules (language, i18n, commits, testing expectations).
|
||||
|
||||
## Overview
|
||||
|
||||
Vue 3 + Composition API single-page app. State in Pinia, routing via vue-router hash mode, styling via Tailwind CSS v4 on top of shadcn-vue primitives (copied in, not imported from a package). No TypeScript, no Bootstrap, no `dark:` dual-pair utilities.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── App.vue ← thin shell, delegates to composables
|
||||
├── main.js ← app bootstrap + global init
|
||||
├── store.js ← Pinia main store
|
||||
├── firebase-init.js ← Firebase Auth env-gated init
|
||||
├── sw.js ← Serwist service worker
|
||||
├── router/ ← Advanced Tools subroutes (open inside a Drawer)
|
||||
├── locales/ ← i18n copy (en / zh / fr / tr) + security-checklist data
|
||||
├── style/style.css ← Tailwind v4 entry + design tokens
|
||||
├── lib/ ← cn() helper (tailwind-merge + clsx)
|
||||
├── data/ ← static config
|
||||
│ (achievements / ip-databases / sections /
|
||||
│ default-preferences / changelog)
|
||||
├── utils/ ← pure helpers
|
||||
│ (valid-ip / getips / transform-ip-data /
|
||||
│ hero-ip-size / fetch-with-timeout / …)
|
||||
├── composables/ ← reusable composition logic
|
||||
│ ├── use-info-mask.js
|
||||
│ ├── use-refresh-orchestrator.js
|
||||
│ ├── use-scroll-to.js
|
||||
│ ├── use-section-tracking.js
|
||||
│ ├── use-shortcuts.js
|
||||
│ └── use-status-tone.js ← shared 4-tier business-state → visual-color mapping
|
||||
└── components/
|
||||
├── *.vue ← top-level sections (IpInfos / Connectivity / WebRTC / DnsLeaks
|
||||
│ / SpeedTest / Advanced / Footer / Nav / Achievements / User /
|
||||
│ Additional)
|
||||
├── ip-infos/ ← IP-card subcomponents (IPCard / IpDetailPanel / ASNInfo / DataPairBar)
|
||||
├── advanced-tools/ ← 10 tool subpages opened inside the bottom Drawer
|
||||
│ (MtrTest / GlobalLatencyTest / RuleTest / DnsResolver /
|
||||
│ CensorshipCheck / Whois / MacChecker / BrowserInfo /
|
||||
│ InvisibilityTest / SecurityChecklist + Empty)
|
||||
├── widgets/ ← small reusables (QueryIP / Help / Preferences / InfoMask / PWA / Toast)
|
||||
├── svgicons/ ← a few inline SVGs
|
||||
└── ui/ ← shadcn-vue copy-in primitives (see "UI system" below)
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Composition API.** All components use `<script setup>`; no Options API.
|
||||
- **Path alias.** `@` → `frontend/` (defined in `jsconfig.json` and `vite.config.js`). Use `@/components/...`, `@/utils/...`, `@/composables/...`.
|
||||
- **Shared-with-backend helpers live under `common/`.** Import them with a relative path, e.g. `../../common/valid-ip.js`. `common/valid-ip.js` is re-exported from `frontend/utils/valid-ip.js` so most consumers can keep using `@/utils/valid-ip.js`; follow that pattern when adding more shared helpers (see `frontend/utils/fetch-with-timeout.js` for the second example).
|
||||
|
||||
## shadcn-vue first
|
||||
|
||||
Before hand-rolling a UI component (button, dialog, popover, list, etc.):
|
||||
|
||||
1. **Check `frontend/components/ui/` first** — 21 primitives are already copied in (see below). Missing variants are almost never a reason to bypass a primitive; `:class` overrides, `as-child`, and tw-merge cover nearly every state-color need.
|
||||
2. **If no local primitive fits, check https://www.shadcn-vue.com/docs/components** — shadcn-vue covers a lot more than what's in the repo. Copy one in if it's a good fit.
|
||||
3. **Only fall back to hand-rolled Tailwind** when the shape or behavior genuinely doesn't exist in shadcn-vue.
|
||||
|
||||
## UI system
|
||||
|
||||
### Primitives
|
||||
|
||||
Located at `frontend/components/ui/`. 21 primitives copied in:
|
||||
|
||||
`accordion` · `badge` · `button` · `button-group` · `card` · `collapsible` · `dialog` (with `DialogHeader`) · `drawer` (vaul-vue) · `dropdown-menu` · `input` · `input-group` · `progress` · `select` · `separator` · `sheet` · `sonner` · `spinner` · `switch` · `tabs` · `toggle-group` · `tooltip`
|
||||
|
||||
Three are project-specific, not in stock shadcn-vue:
|
||||
|
||||
- **`ButtonGroup`** — visual container for stitching buttons together.
|
||||
- **`InputGroup`** — flattens `Input + Button` (or `Input + Select`) into a single bordered control via `[&>input] / [&>button]` passthrough selectors. The group itself carries border / ring / shadow. Use this whenever an input is paired with a trigger button — never hand-roll `rounded-r-none / -ml-px`.
|
||||
- **`Spinner`** — lucide `Loader2` + `animate-spin` + `role="status"`.
|
||||
|
||||
### Design tokens
|
||||
|
||||
Tokens live at the top of `frontend/style/style.css`. On top of shadcn defaults, four business-semantic colors are defined:
|
||||
|
||||
| Token | Meaning | Light | Dark |
|
||||
|---|---|---|---|
|
||||
| `--info` | waiting / in-progress | sky-500 | sky-400 |
|
||||
| `--success` | ok-fast / success | green-500 | green-400 |
|
||||
| `--warning` | ok-slow / warning | amber-500 | amber-400 |
|
||||
| `--action` | the "run / trigger" brand color | blue-500 | blue-500 |
|
||||
|
||||
`--destructive` uses the shadcn default. Every token has a paired `-foreground`; `bg-*` consumers do not need `dark:` variants.
|
||||
|
||||
Rule: use semantic tokens (`bg-info` / `bg-success` / `bg-warning` / `bg-destructive` / `bg-action` / `bg-muted` / `bg-card` / `bg-accent` / `text-foreground` / `text-muted-foreground` / `border` / …). Do not write `dark:` dual pairs like `bg-neutral-50 dark:bg-neutral-900` — that pattern does not exist in this codebase.
|
||||
|
||||
### Button and Badge variants
|
||||
|
||||
Button adds `action` (blue-500 trigger color) to the shadcn defaults (`default / destructive / outline / secondary / ghost / link`).
|
||||
|
||||
Badge adds `success` to the defaults. Badge hover is globally disabled — Badge is a display element, not an interactive one. Put `hover:bg-muted` etc. on an outer `<a>` / `<button>` when interactivity is needed.
|
||||
|
||||
### Status tones
|
||||
|
||||
All "business state → visual color" mapping goes through `frontend/composables/use-status-tone.js`. It exposes four tones: `wait` / `ok-fast` / `ok-slow` / `fail`.
|
||||
|
||||
Rule: any new module that surfaces a status reuses these tones. Do not hand-write `switch` statements that map states to hex codes or Tailwind classes.
|
||||
|
||||
### Canonical patterns
|
||||
|
||||
**Trigger button** — `variant="action"` + `<Spinner />` + `:disabled` is the three-piece pattern used by QueryIP, MacChecker, DnsResolver, Whois, MtrTest, Ping, Censorship, Invisibility, etc.
|
||||
|
||||
```vue
|
||||
<Button variant="action" :disabled="isRunning" @click="run">
|
||||
<Spinner v-if="isRunning" />
|
||||
Run
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Input with trigger** — `InputGroup` flattens the pair. `SelectTrigger` renders as a button, so `Select` can swap in for `Input` with no extra work.
|
||||
|
||||
```vue
|
||||
<InputGroup>
|
||||
<Input v-model="q" placeholder="..." />
|
||||
<Button variant="action" :disabled="running" @click="run">
|
||||
<Spinner v-if="running" /> Run
|
||||
</Button>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
**Status card** — homepage status cards (Connectivity / WebRTC / DnsLeak / IPCard / RuleTest) use:
|
||||
|
||||
```vue
|
||||
<Card class="keyboard-shortcut-card jn-card transition-transform duration-300 ease-out hover:-translate-y-1.5">
|
||||
```
|
||||
|
||||
`jn-card` is the project marker for shadow / border / keyboard-nav outline. `keyboard-shortcut-card` is the marker that the J/K keyboard-shortcut navigation queries for.
|
||||
|
||||
**Flag** — always use the iconify circle-flags set:
|
||||
|
||||
```vue
|
||||
<Icon :icon="'circle-flags:' + code.toLowerCase()" class="size-4" />
|
||||
```
|
||||
|
||||
**Tables vs lists** — two or three columns with independent header semantics → `<table>` with `thead text-xs uppercase tracking-wide text-muted-foreground` + `tbody divide-y` + row `hover:bg-muted/50`. Mobile-friendly rows or asymmetric content with no header meaning → `<ul class="rounded-lg border bg-card divide-y">` + `<li>`.
|
||||
|
||||
**Dialog header** — use the `<DialogHeader :icon="SomeIcon" :title="..." />` primitive to get an icon, title, and DialogClose in one element, matching the Sheet / Drawer header treatment.
|
||||
|
||||
**Drawer** — reserved for the bottom drawer (Advanced Tools panel), via `vaul-vue`. Side panels (right / left) use `Sheet`. The Drawer defaults to `handleOnly: true` (only the top handle is draggable) and CSS is set so body text remains selectable.
|
||||
|
||||
**Motion** — card hover lift: `transition-transform duration-300 ease-out hover:-translate-y-1.5`. Panel fade-slide: scoped `.fade-slide-*` transitions. Loading: `<Spinner />`, not hand-rolled `animate-pulse` dot clusters.
|
||||
|
||||
## Testing
|
||||
|
||||
- Composables and utils are the main target — covered by `tests/composable-*.test.js` and the various utility tests.
|
||||
- Vue rendering / user interactions / browser APIs: out of scope for the Node test runner.
|
||||
- For visual changes (layout, styling, animations), self-testing is not possible — explicitly ask the user to verify via `npm run dev`.
|
||||
Reference in new issue
Block a user