Skip to content

[pull] main from jason5ng32:main#106

Merged
pull[bot] merged 59 commits intoCosr-Backup:mainfrom
jason5ng32:main
Apr 24, 2026
Merged

[pull] main from jason5ng32:main#106
pull[bot] merged 59 commits intoCosr-Backup:mainfrom
jason5ng32:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 24, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

jason5ng32 and others added 30 commits April 19, 2026 17:14
Toast had been wedged into the bottom-*left* corner because the two
floating action buttons (InfoMask + QueryIP) occupied the conventional
bottom-right real estate. That was a downstream cost of the FAB choice
and felt off — every app in the sonner/react-hot-toast generation puts
toasts bottom-right by default.

Keep the FABs where they are and move the toasts around them instead.
The new rule lives in Toast.vue as an unscoped block (sonner renders its
root to document.body, so scoped styles wouldn't reach it):

  right: calc(max(0px, (100vw - 1600px) / 2) + 66px) !important;

The `(viewport - 1600) / 2` half-overflow term tracks the same content-
edge math InfoMask / QueryIP use in their `positionStyle`, so the toast
stays glued to the left side of the FAB column on every screen size:

  narrow (≤1600px): flat 66px from viewport right (FAB width 36 +
                    gap 10 + FAB's own 20 right-margin = 66)
  wide   (>1600px): 66px + half the 1600-clamped overflow, matching
                    where the FAB column drifted inward

Vertically the toast keeps sonner's default 32px offset — no longer
climbing above the FABs, just side-stepping them.

FAB vertical positions were nudged by 4px each (bottom-5 → bottom-6,
bottom-[66px] → bottom-[70px]) for a touch more breathing room between
them and the screen edge. Horizontal positioning unchanged, so the 66
in the CSS above still matches.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The Network Connectivity section now ships with 7 built-in targets plus
a user-editable slot. Users can add up to 9 custom tests (name + URL),
each tested with the same favicon-load method as the built-ins and
persisted to `userPreferences.customConnectivityTargets` in localStorage.

Highlights:

- The existing `connectivityTests` reactive array is the single source
  of truth; custom entries carry a `custom: true` flag and everything
  else (J/K navigation, `checkAllConnectivity`, status rendering) treats
  them identically to built-ins.
- A watch on the stored preference does a diff-based reconciliation
  (drop removed ids, push new ids, leave survivors in place) so that
  adding or removing one target does NOT reset the status/time/mintime
  of the others — a cosmetic regression I hit in an earlier draft.
- Custom cards render a first-letter colored tile instead of a lucide
  icon. Color is a stable hash of the name, so "Weibo" always shows
  the same shade across sessions / devices.
- The "+" add tile sits at the end of the grid and hides itself once
  the 9-target cap is reached.
- Inline × on hover removes a custom card. Built-in cards are not
  removable — the `v-if="test.custom"` gates the button.
- URL normalization (`weibo.com` / `www.weibo.com` / full URL with
  path+query) collapses everything to `origin/favicon.ico?` so the
  existing `checkConnectivityHandler` can append its cache-bust
  suffix unchanged.
- Add dialog uses a new shadcn-vue `Label` primitive (drop-in copy
  matching the library convention) for the two form fields. Errors
  toggle `aria-invalid` on the offending input for the red ring, and
  the error paragraph has a 1-line min-height reservation so toggling
  errors doesn't shift the dialog height.
- `DEFAULT_PREFERENCES.customConnectivityTargets = []` plus a test
  update so shape assertions still match.
- Four locales get the new `connectivity.addCustom.*` keys (title,
  labels, placeholders, hints, error strings, add/remove actions).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…o query inputs

Two accessibility papercuts resolved together.

1. Every Dialog in the app printed a reka-ui warning in the console on
   open:
     Warning: Missing `Description` or `aria-describedby="undefined"`
     for DialogContent.
   The shadcn-vue DialogContent primitive already had a VisuallyHidden
   fallback for DialogTitle but none for DialogDescription, so screen
   readers had no aria-describedby target and reka-ui refused to stay
   quiet about it. Adding a symmetric `description` prop / slot plus
   a fallback that reuses the title string satisfies the contract
   without forcing every call site to pass redundant copy. Optional
   callers can still override via `:description="..."` or the
   `description` named slot when they want a distinct spoken label.

2. Propagate the aria-invalid-on-error pattern (introduced in the
   Connectivity Add dialog) to the other query inputs that have
   inline error messages: QueryIP, CensorshipCheck, DnsResolver,
   MacChecker, Whois. With shadcn-vue's built-in `aria-invalid:*`
   Tailwind utility, the offending field now gets the red ring
   automatically whenever its errorMsg / modalQueryError is non-empty,
   giving users a second, visual cue beyond the text message.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ients

V6.0 reshaped several default preferences. Existing users have their old
overrides merged over new defaults (`{ ...defaults, ...stored }`), which
means they'd silently keep stale values for any option that used to exist
under a different shape or meaning. We want the release to force everyone
onto a clean baseline.

Rename the key from `userPreferences` → `userPreferences_v6`:

- Old key turns into orphaned data — the loader can't see it, falls back
  to `createDefaultPreferences()`, fresh state wins.
- On that same fresh-install branch we `removeItem(legacy)` so browsers
  don't carry dead bytes forever (guarded behind "new key was empty" to
  avoid racing a concurrent tab that already migrated).
- `PREFS_STORAGE_KEY` + `LEGACY_PREFS_KEYS` constants sit at the top of
  store.js so the next release can just bump the suffix if it needs the
  same treatment. The comment above them documents when to do this.
- `frontend/locales/i18n.js` was also reading the raw key string in
  `setLanguage()`; updated in lockstep (the comment there cross-refs the
  store constant).
- Removed a now-redundant `localStorage.setItem(...)` in `loadPreferences`
  that duplicated what `setPreferences()` already does on the next line.
- `tests/store.test.js` asserted against the literal old key in 4 places;
  all updated to the new key.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Cuts the first minor release since v6.0's rewrite. Four changelog
entries picked for user-facing visibility:

- Network Connectivity accepts up to 9 user-added test targets
  (saved locally in the browser).
- Toasts returned to the bottom-right corner, offset around the
  FAB column.
- Auto theme mode now follows OS light/dark flips instantly
  (previously needed a reload / manual toggle).
- First-paint visual flicker on page load removed.

package.json bumped 6.0.0 → 6.1.0 in lockstep. Date stamped for
Apr 20, 2026 to match the planned release day.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
On mobile (iOS Safari in particular, real device), tapping a category
in the Security Checklist sidebar made the whole vaul drawer visually
slide up — the header disappeared off the top, a matching blank patch
appeared at the bottom. PC didn't repro. Chrome DevTools mobile mode
didn't repro. Only manifested on real iOS.

Root cause: `changeList` called
`document.getElementById('checklist').scrollIntoView({ block: 'nearest' })`
on the details pane. On desktop's md 3-column grid the target was
already visible and the call no-op'd. On a 1-column mobile layout the
browser walked up DOM for the nearest scrolling ancestor to move, and
in settling the result it ended up touching the root scroller — but
vaul keeps the page scroll-locked via `body { position: fixed;
top: -Npx }` while the drawer is open, and that nudge shifted its
recorded offset. Visually the drawer itself looked like it slid.

Fix: walk up from the target element ourselves to find the first
ancestor with `overflow-y: auto | scroll`, check whether the target is
already inside that scroller's viewport (preserves the desktop no-op),
and if not, `scrollBy` directly on that element. The scroll never
bubbles past the drawer's inner container, so vaul's scroll-lock state
stays untouched.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Real iOS PWA reproducibly mis-rendered the Whois input caret a full
line below the input on the Chinese locale — text landed inside the
input but the `|` caret painted in the empty space below. Only
happened in PWA (not Safari), only in Chinese (not English), and only
on Whois (not Censorship / DNS Resolver / MAC Lookup, even though
they shared the exact same markup pattern).

The culprit turned out to be the raw `<label for="queryURLorIP">`.
Swapping it for the shadcn Label primitive (reka-ui + Tailwind classes:
`flex items-center text-sm leading-none font-medium select-none ...`)
fixed the caret instantly. The likely reason is a combination of:

- `select-none` removing the label from WKWebView's selection-range
  calculations, so it no longer contributes to the input's caret Y
  coordinate;
- `leading-none` making the label's line-box tight around the font,
  which removes the CJK-specific overflow the default `text-sm`
  line-height caused;
- reka-ui's Label primitive handling pointer events differently
  from a naked `<label>`.

Why Whois was uniquely affected while the three siblings weren't is
probably a threshold issue — Whois's top Note paragraph was the only
one without `leading-relaxed`, so accumulated CJK line-box overflow
upstream tipped it past the bug's trigger point.

While touching these files:
- Applied the same shadcn Label swap to the other three tools
  (Censorship / DNS Resolver / MAC Lookup) for consistency, so the
  same latent bug can't surface on those inputs in the future.
- Added `leading-relaxed` to the top Note paragraphs that were missing
  it (Whois / MAC Lookup).
- Added the `autocomplete / autocorrect / autocapitalize=off` +
  `spellcheck=false` attrs on Whois and MAC Lookup inputs — orthogonal
  hygiene for technical-string fields (domains, IPs, MAC addresses
  don't benefit from iOS text correction).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- QueryIP: skip programmatic focus on iOS. Inside a fixed-position Dialog,
  iOS Safari cannot scroll a programmatically-focused input into view above
  the on-screen keyboard. Letting the user tap the input delegates the
  whole keyboard + visualViewport dance to iOS natives. Desktop still
  auto-focuses on open.
- QueryIP: replace deprecated `navigator.platform` with a Macintosh UA +
  `maxTouchPoints` check so iPadOS 13+ is still detected.
- Apply a consistent attribute set (`autocomplete="off" autocorrect="off"
  autocapitalize="off" spellcheck="false" data-1p-ignore data-lpignore`)
  to every free-form Input (QueryIP, Whois, MacChecker, CensorshipCheck,
  DnsResolver, ConnectivityTest × 2). iOS QuickType was pushing
  iCloud-address and password AutoFill onto URL/IP/MAC inputs.
- Rename ipcheck.Placeholder from \"Please enter an IP address\" style to
  \"e.g. 1.1.1.1\" in all four locales. iOS address-AutoFill heuristics
  fire on the word \"address / 地址 / adresse / adresi\" even with
  `autocomplete=\"off\"`.
- Document the six-attribute convention in frontend/AGENTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Required for local testing against dev.ipcheck.ing / test.ipcheck.ing;
Vite rejects Host headers that aren't on `server.allowedHosts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the scattered length-threshold helpers (heroIpSizeClass in
utils/, three copy-pasted fitOneLineClass functions across WebRtc /
DnsLeak / RuleTest) with a single composable + wrapper component that
measures real layout:

- useFitText observes the element's parent via ResizeObserver and
  picks the largest Tailwind font-size tier whose rendered extent
  fits (scrollWidth for single line, scrollHeight for line-clamped
  multi-line). Recomputes on container resize and text change.
- HERO_TIERS (xl → xs) for the prominent IPCard + QueryIP rows;
  INLINE_TIERS (base → xs) for the narrower test cards.
- FitText wraps useFitText so v-for call sites get per-iteration
  observers without plumbing refs themselves. A #prefix slot lets an
  inline Monitor icon ride the first wrapped line instead of sitting
  at the visual center of a 2-line block.
- IPCard / QueryIP opt into max-lines=2 so long IPv6 wraps rather
  than shrinking below text-sm on narrow mobile cards. Copy stays a
  flex sibling so the ellipsis clips only the IP, never the button.
- DnsLeaksTest dropped a vestigial v-if/v-else pair and its unused
  isResolved() that only served the collapsed branch.
- frontend/AGENTS.md gains a Fit-to-width section and lists the new
  module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before, `Promise.all` waited for the slowest DNS/DoH server; a single
unreachable provider could pin the response at 20+ seconds (Node's
default 5s timeout × 4 retries). Now:

- UDP DNS uses `new Resolver({ timeout: 3000, tries: 1 })`.
- DoH switches from bare `fetch` to `fetchUpstream` with `timeoutMs:
  5000`, aligning with the project convention in api/AGENTS.md that
  all upstream HTTP calls go through the timeout wrapper.

Slow / unreachable servers now resolve to `N/A` inside ~5s instead of
tens of seconds, capping total response time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
whoiser's port-43 WHOIS path returns empty for newer gTLDs (.ing /
.app / .dev / …) that expose RDAP only. Keep whoiser as the primary
path — it still returns richer multi-provider text for legacy gTLDs
like .com / .us — and fall back to a minimal in-house RDAP client
(common/rdap.js) when whoiser produces no __raw for any provider.

The fallback:

- loads IANA's TLD → RDAP endpoint map (data.iana.org/rdap/dns.json)
  with a 24h in-memory cache
- issues one RDAP GET via fetchUpstream (5s cap)
- formats the RDAP JSON into a WHOIS-like text block (status, events,
  entities-by-role with vCard name / email / address, nameservers,
  DNSSEC)
- returns the same { [host]: { __raw, ... } } shape as whoiser.domain()
  so the frontend Accordion renders it with zero changes

IP queries stay on whoiser only — regional IP WHOIS servers are
widely reachable and RDAP adds no coverage there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap ClipboardCheck / ClipboardPlus for CopyCheck / Copy (the pair
used everywhere else in the app for copy affordances) and apply the
same size-4 + text-success post-copy tint the IPCard copy button
uses, so the UA copy button looks and behaves consistently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CircleCheck / CircleX on the Native IP row was always rendered
in text-muted-foreground, which made the status glyph visually
indistinguishable from the row label. Let it inherit the default
text color so the check / cross carries its own weight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AI assistants and human contributors alike should always base work on
`dev` and commit back to `dev`; `main` is only reached via PRs merging
`dev` → `main`. Also codifies the worktree-aware way to fast-forward
`dev` from inside a secondary worktree: `git push . HEAD:dev` (with
`receive.denyCurrentBranch=updateInstead` configured on the repo, so
the main worktree's files auto-sync when clean) instead of
`git update-ref`, which otherwise leaves the main worktree's HEAD and
working tree out of sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
<img>-based probing has two failure modes that routinely produce
misleading results:

- Any non-2xx HTTP response (notably github.com/favicon.ico's 403 from
  CDN hot-link rules) triggers onerror, flagging a reachable origin as
  unavailable.
- The timing necessarily covers full-body download; favicons that ship
  as 50-100 KB SVG/PNG blow up the measured latency well past the
  actual RTT.

fetch(url, { mode: 'no-cors', method: 'HEAD', cache: 'no-store' })
fixes both: no-cors resolves the promise on any HTTP status (403
included), and HEAD returns only headers so payload size drops out of
the measurement. An AbortController replaces the bare setTimeout and
closes the old race where both onerror and the timeout could fire on
the same probe. Timing switches to performance.now() for float-ms
precision.

Bundled URL cleanups in the same change: strip the legacy trailing
"?" (cache-bust placeholder, superseded by cache: 'no-store') from
all built-in targets, and rename normalizeToOriginUrl →
normalizeTestUrl — it now preserves user-typed paths (so
"example.com/api/health" actually probes /api/health) and only
defaults to /favicon.ico when the user gave a bare domain, since "/"
routinely forces per-site backend logic that inflates HEAD latency
on major CDNs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude and others added 25 commits April 20, 2026 15:41
5 in-app sites still used bare fetch(), leaving them vulnerable to
indefinite hangs on slow DNS / upstream flake:

  - DnsLeaksTest (edns.ip-api.com + surfsharkdns.com leak probes)
  - WebRtcTest  (MaxMind country lookup for discovered STUN IPs)
  - SpeedTest   (Cloudflare cdn-cgi/trace)
  - IpDetailPanel (/api/cfradar)
  - store.fetchConfigs (/api/configs)

Swap each for fetchWithTimeout from @/utils/fetch-with-timeout.js (default
5s timeout, AbortController-backed). ConnectivityTest's HEAD + no-cors
probe at line 272 stays as-is — its manual AbortController is part of the
reachability-testing design, noted in the refactor plan.

Co-authored-by: Claude <noreply@anthropic.com>
DnsResolver / Whois / CensorshipCheck each inlined the same hostname
regex (/^[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i) to decide whether a
parsed URL's hostname was domain-shaped. Promote it to a named
isValidDomain() export alongside isValidIP() — same re-export pattern
through frontend/utils/valid-ip.js — and update the three call sites.

Tests cover common + frontend entry points in parallel, matching how
isValidIP is already verified.

Co-authored-by: Claude <noreply@anthropic.com>
Three components had async work that could fire after the component was
gone, either writing refs on a dead instance or holding references the
engine side:

  - ConnectivityTest: the autoRefresh setInterval was never cleared on
    unmount. Also change intervalId's initial value from 3000 (a stale
    leftover) to null so the cleanup guard reads correctly.
  - Advanced.vue: the 1500ms setTimeout that unlocks the Invisibility
    card never had its id tracked, so a fast route-change would leave
    the callback in flight. Store the id and clearTimeout on unmount.
  - SpeedTest: the existing onUnmounted nulled testEngine but the
    SpeedTestEngine's own async work would still invoke
    onRunningChange / onResultsChange / onFinish / onError on detached
    refs. Null the callbacks first, matching what onFinish already does
    at normal completion.

Co-authored-by: Claude <noreply@anthropic.com>
signInWithGoogle and signInWithGithub wrote this.alert = {...} directly,
bypassing the setAlert() action that every other call site uses. Route
them through setAlert so the alert mutation stays on one path.

Co-authored-by: Claude <noreply@anthropic.com>
…escription

Two browser a11y warnings fired every time an Advanced Tools Drawer opened:

1. "Blocked aria-hidden on an element because its descendant retained
   focus" — clicking an Advanced Tools card kept focus on the card;
   when the Drawer opened, vaul tagged #mainpart (the card's ancestor)
   with aria-hidden="true", so the focused card sat inside an
   aria-hidden subtree. navigateAndToggleOffcanvas now blurs the click
   target before router.push, releasing focus so the descendant stays
   out of the hidden region.

2. "Missing Description or aria-describedby=\"undefined\" for
   DialogContent" — vaul-vue's DrawerContent renders reka-ui's Dialog
   primitive, which now requires a description. Add a VisuallyHidden
   DrawerDescription fallback that reuses the title when no
   description prop / slot is passed — same pattern the sibling
   DialogContent.vue already uses.

Co-authored-by: Claude <noreply@anthropic.com>
The earlier commit silenced the "aria-hidden on focused descendant"
warning at open time by blurring the click target, but Tab-focusing
back into #mainpart while the Drawer was open brought the warning
right back — aria-hidden alone doesn't remove descendants from the
browser's focus ring.

Bind :inert on #mainpart to store.openSheet !== null. inert is the
spec-correct mechanism (explicitly recommended by the browser warning
itself): it blocks focus, input, and pointer events on the subtree, so
Tab skips past any main-content focusable while an overlay is up.

Co-authored-by: Claude <noreply@anthropic.com>
The STUN test had two related correctness problems:

  1. candidateReceived flipped to true on *any* candidate (including
     host), so the 5s timeout's !candidateReceived guard almost never
     fired. With Chrome / Firefox mDNS privacy on, host candidates are
     abc.local hostnames that don't match the IP regex — stun.ip was
     never updated, but the timeout backstop was already defeated, and
     the row stayed on "Awaiting Test" forever. This is what prompted
     the user report.

  2. When mDNS privacy was off, the host candidate's local IP
     (192.168.x.x) *did* match the regex — so the row proudly displayed
     an internal IP as the "STUN server IP", implying STUN worked when
     it hadn't.

Only server-reflexive / peer-reflexive candidates actually represent a
STUN answer. Filter on type ∈ {srflx, prflx} before accepting an IP.

Once we stop treating host candidates as success, the 5s backstop
becomes the sole non-happy path — worth distinguishing:

  - StatusPrivacy: every candidate we saw was an mDNS .local host,
      AND no srflx ever arrived → browser mDNS privacy is on AND STUN
      didn't answer. More informative than generic "error".
  - StatusTimeout: saw non-mDNS candidates (or none at all) but no
      srflx → STUN server unreachable / UDP blocked / wrong URL.
  - StatusError: unexpected throw (RTCPeerConnection construction etc).

Add locale keys webrtc.StatusTimeout / StatusPrivacy across en / zh /
fr / tr. Centralize the non-wait label set via an errorStatusLabels
computed so toneOf (via ipFieldTone) and isFieldPending stay in sync.

Also plug two small timer issues from the old shape:
  - clearTimeout the 5s backstop on success (no stale closure).
  - guard against double-settle via a single `settled` flag shared by
    the success / failure paths.

Co-authored-by: Claude <noreply@anthropic.com>
…defined

The old flow had fetchCountryCode return undefined on both the empty-
response and caught-error branches. The caller then did countryInfo[0],
got a TypeError, and relied on an outer try/catch to swallow it and
stamp the "Error" country — a correct-by-accident loop through an
exception that wasn't actually an exception.

Switch to an explicit null return on every non-happy path (missing
MaxMind source, empty upstream payload, network failure). succeedWith
now checks once, drops its now-redundant try/catch, and surfaces the
StatusError label directly.

Co-authored-by: Claude <noreply@anthropic.com>
Matching the P1.2 sweep: if the component unmounts mid-test (user
navigates away, route change, etc.) the RTCPeerConnection keeps ICE
gathering for seconds and eventually tries to write to stun row refs
that no longer belong to any live component.

Track every pc created inside checkSTUNServer in an activeConnections
Set, remove on close, and close the whole set in onBeforeUnmount.

Co-authored-by: Claude <noreply@anthropic.com>
Revert 3a9ba2a and c448fae — user decided the Drawer a11y warnings
are not worth fixing. The inert binding on #mainpart, the click-
target blur in Advanced.vue, and the DrawerDescription fallback are
all rolled back.

This reverts the following commits:
  3a9ba2a Fix(a11y): mark #mainpart inert while any overlay is open
  c448fae Fix(a11y): silence Drawer aria-hidden warning and add
          DialogContent description

Co-authored-by: Claude <noreply@anthropic.com>
…rror

a401f78 tried to distinguish "5s timeout" from "mDNS privacy" based on
whether every observed candidate was a .local host. In practice the
two conditions are independent: Chrome / Firefox default to mDNS
privacy on, so a genuine STUN URL failure still gets reported as
StatusPrivacy whenever all host candidates happen to be mDNS — which
is almost always. The split produces false positives and misleads
more than it informs.

Collapse back to a single vague StatusError for every non-success
path. Keep the srflx / prflx filter (host candidates still aren't a
STUN success signal) and the settle / timer-clear plumbing from
a401f78 — those are orthogonal correctness fixes.

Drop the now-unused StatusTimeout / StatusPrivacy locale keys across
en / zh / fr / tr, and simplify the tone / pending checks back to
comparing against the two original labels.

Co-authored-by: Claude <noreply@anthropic.com>
…EAVD

Refactor(frontend): script-layer audit — composables, fetch timeouts, STUN fixes
New advanced tool at /enhanceddnsleaktest: generates a 32-hex session
token client-side, fires four <img> DNS probes in parallel, then pulls
the captured recursive-resolver rows (IP / ASN / geo / ECS / DNSSEC
DO+CD) from the main IPCheck.ing API. Firebase sign-in + configs.originalSite
gated, matching the InvisibilityTest pattern.

- api/dns-leak-test.js — thin proxy to
  GET {IPCHECKING_API_ENDPOINT}/dnsleaktest/session/:token, forwarding
  Authorization and attaching the apikey query param; upstream status
  and body passed through verbatim so the frontend can surface
  "Sign in required" / "Invalid token".
- EnhancedDnsLeakTest.vue — full client flow: parallel probe firing
  with sequential UI animation, single-region flow card (status dot +
  stage text + percent + shadcn Progress), sortable / dedupable result
  Table with ECS-first, country-grouped, numeric-IP ordering, DNSSEC
  DO/CD rendered as green check / amber cross, field-reference legend.
- Homepage DNS Leak Test gains a fade-slide banner that surfaces the
  deeper tool once the basic test settles (sticky, won't retract on
  re-run) and a new D keyboard shortcut.
- Shadcn Table primitive copied in (via shadcn-vue add table).
- i18n: full 4-locale coverage for the enhanced tool, the banner, the
  advancedtools card blurb, and the D shortcut description.
- Tests: dns-leak-test smoke coverage added; configs test expectation
  aligned with restored handler.
- AGENTS.md: sync api/ and frontend/ docs with the new handler, new
  advanced tool, new Table primitive, and the extra private-API
  header-passthrough consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching the fetch probe from HEAD to GET. Many servers / CDNs / WAFs
silently drop HEAD, route it differently, or close the connection — in
no-cors mode that rejects the promise and falsely marks reachable sites
as unavailable. User-added custom endpoints are especially prone to this
because most health-check / API paths only implement GET.

The payload overhead (a few KB of favicon on a CDN edge, discarded as an
opaque response) is a small price for the correctness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously reloadMaxMindDatabases ran fire-and-forget at the top level
and only logged an error when the .mmdb files were missing — the server
would start but MaxMind API returned 503 forever until someone noticed.

Now backend-server.js runs an explicit bootBackend() that:
  1. bootstrapMaxMindIfMissing — if files are absent, check for
     MAXMIND_ACCOUNT_ID / MAXMIND_LICENSE_KEY and either print a clear
     "how to fix" warning or run a synchronous download cycle capped at
     5 minutes (AbortController threaded through fetch + pipeline so the
     abort actually stops in-flight I/O).
  2. reloadMaxMindDatabases — load readers.
  3. startMaxMindFileWatcher + startMaxMindAutoUpdate — unchanged.
  4. app.listen — only after the steps above.

The 5-minute cap is a total timeout only; we deliberately skipped the
"no-bytes-for-N-seconds stall detector" to keep the path simple. On any
failure (bad credentials, blocked outbound, timeout) the server still
starts — just with MaxMind returning 503 and a clear operator warning.

MAXMIND_AUTO_UPDATE is intentionally NOT consulted by bootstrap; that
flag only gates the periodic scheduler. Credentials alone are enough
signal that the operator wants MaxMind working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace all backend console.* with a shared pino singleton at
common/logger.js. Startup-style lines get emoji prefixes (🚀 listening,
📦 maxmind ready, 📥 downloading, 🗓️ schedule, ⚠️ warning, ❌ failure);
per-handler / per-request logs stay plain.

Three .env-driven knobs, all without NODE_ENV:
  - LOG_LEVEL (default info) — pino level filter
  - LOG_FORMAT (default pretty) — "json" for log shippers
  - LOG_HTTP (default off) — pino-http on /api/* gated behind this flag
    so pm2 logs don't bloat with 2xx/3xx request lines on every deploy;
    when enabled, 4xx log as warn and 5xx as error

logger.js calls dotenv.config({ quiet: true }) itself because ES module
imports are hoisted above backend-server.js's dotenv call — without that
LOG_LEVEL from .env was silently ignored.

Fixes in common/rdap.js:
  - err: res.status → status: res.status (pino's err serializer expects
    an Error object, not a number)
  - broken single-quoted template literal in "No RDAP endpoint" message
  - demote "No RDAP endpoint" and "Domain not found" from error to warn
    (they're expected outcomes for user input, not server failures)

README (en/zh/fr/tr) + .env.example + AGENTS.md (root and api/) all
updated to document the three new vars and the "no console.* in backend"
convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed overlay (position: fixed) avoids mobile 100vh scrollbars, and
App.vue hands off in three stages — text fade, logo shrink, then
reveal #app with a scale-in — instead of display:none'ing the boot
screen instantly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Apr 24, 2026
@pull pull Bot added the ⤵️ pull label Apr 24, 2026
@pull pull Bot merged commit 2951bfc into Cosr-Backup:main Apr 24, 2026
2 of 3 checks passed
@4everland 4everland Bot requested a deployment to production April 24, 2026 18:27 Abandoned
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants