common/ had become the backend's library: 30 of its 41 modules were
imported by nothing in the frontend, so the name no longer said what
belonged there. It now holds only what the frontend or the build imports
too; backend-only code moved to server/, flat except for datasets/ (the
dataset engine and its readers). The *-db/ dataset directories stay in
common/: self-hosting docs and Docker volumes mount those paths.
frontend/utils/ keeps its app-wide modules at the root and gains ip/
(IP / ASN shaping and validation), report/ (the report pipeline) and
features/ (the pure layer of a single feature). Paths only — no logic
changes. The Dockerfile copies server/, and the AGENTS.md files say where
a new module goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full profile of one autonomous system at /tools/asn?q=AS<n>, served by
a single /api/asn-profile request: identity and registration with the raw
Whois record, key facts (rank, announced IPv4, prefixes, users), traffic
shares and connection quality, where its IPv4 space is used, the announced
prefix list with RPKI status, and connectivity with the topology graph.
A reputation card appears beside the header when the backend returns it.
- The ASN Info panel now opens the profile instead of linking out.
- ASNConnectivity gains `expandable` / `bordered` so the profile can embed
it without the nested drawer and the second border.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Registry gate `requiresConfig`: a tool is listed in the Advanced Tools
grid and the nav only when its backend config is present; the listing
filter is one pure function. ASN Profile requires the Cloudflare key.
- `useAsnInfo` owns the Radar ASN fetch and its session cache, shared by
the IP cards and the query dialog.
- ASN Info's traffic-share and connection-quality blocks become small
components over pure helpers, reusable by the profile page.
- ASN Profile lands as a mock-data page for now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Split CountryTraffic and LocationMap into bare content panels plus
dialog shells, and turn the enableMap/enableCountryTraffic booleans
into mapMode/countryTrafficMode ('dialog' | 'inline' | 'off'). IPCard
keeps the dialogs; QueryIP — itself a Dialog, where stacking another
one is confusing — now offers both affordances as a single-select
Collapsible under the metadata grid, mirroring its ASN panels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A source declining with 403 is a fail-over, not a defect — keep it below
the level error monitoring captures, while every other failure stays an
error so a real outage still surfaces.
One registry (common/locale-registry.js) now owns the locale list; the
lazy-load maps, picker options, <html lang>, zh->zh-CN mappings and test
locale lists all derive from it, so adding a language becomes one JSON
pack plus one registry line. Browser-language detection matches exact
tags before base languages, clearing the path for regional packs like
zh-TW. Pure refactor: behavior for the four shipped locales is unchanged
except the picker now renders in registry order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QueryIP, MtrTest and GlobalLatencyTest all took any well-formed IP and
spent a round trip discovering that nobody can answer for it: a LAN
address walks every geo source collecting the same "no data", and no
Globalping probe has a path to one from any country.
All three now answer locally. The two Globalping tools share
classifyTarget(), which returns empty / invalid / unreachable / ok so the
input can both style itself and say why a well-formed address was
refused — a silently greyed-out Run button reads as a bug. QueryIP also
drops its error message once the box is emptied, since the complaint no
longer describes anything on screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extract a single fixed flex-column dock for the bottom-right floating
buttons (InfoMask / QueryIP). Removes the duplicated positionStyle +
resize listeners from each button and the hand-tuned per-button
bottom-* offsets; vertical stacking now comes from flex gap, so hiding
InfoMask no longer leaves a gap above QueryIP. Layout is preserved
(button centers at 56px / 106px, 50px apart).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It is a module-level analytics service with no Vue reactivity — the use- prefix wrongly implied a composable. Renamed utils/use-analytics.js to utils/analytics.js and updated all 29 importers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New /api/asn-connectivity returns a layered graph of upstream paths from
the origin AS toward Tier 1 ISPs (bgp.tools-style). Rendered via dagre +
SVG with Manhattan routing and an expand-into-dialog action.
Backend is fully offline. Two new local snapshots:
- common/as-org-db CAIDA as2org (ASN → org name)
- common/as-rel-db CAIDA as-rel2 (p2c relationships, Tier 1 set)
common/caida-updater handles bootstrap + periodic refresh for both
datasets in one vendor-scoped file, mirroring maxmind-updater. New
common/decompress unifies gzip / bzip2 stream decompression so future
formats are one switch case rather than a new dependency per updater.
Tier 1 membership is derived from CAIDA (no providers + customer cone
>= 100), replacing the hand-rolled list. Origins that are themselves
Tier 1 are flagged so the frontend can render a combined origin+Tier 1
box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fallback loop in QueryIP's fetchIPForModal silently relied on a
JS truthy-check bug — `if (sourceID && source.id !== sourceID)` only
falls through to other sources when sourceID is 0 (which happens to
be IPCheck.ing). For any other preferred source (ipinfo, ip-api,
etc.), the filter activates and the loop tries exactly one source,
so a 5xx from that source left the spinner running forever with no
visible error.
Replace with the same cyclic walk IpInfos.fetchIPDetails uses:
findIndex by id, walk (currentIdx + 1) % sources.length on failure,
attempts < sources.length termination. 200 responses with all-"N/A"
fields are treated as valid output (upstream is healthy, just no
data for this IP) rather than triggering fallback.
When every source genuinely fails, surface a new ipcheck.NoData
error string across en/zh/fr/tr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /api/asn-history handler queries RIPEstat routing-history for an IP
and enriches each historical ASN with its holder name via as-overview
(parallel, best-effort — org=null on timeout, ASN timeline still ships).
Filters out leak-grade prefixes (< /8 v4, < /19 v6) and low-visibility
announcements (< 30 full-feed peers) before fetching org names.
UI: ASN row in IpDetailPanel now has two view-toggles (Info / History)
sharing one Collapsible; at most one open at a time. Session-cached
per IP. Locale keys added in en/zh/fr/tr; bumps version to 6.2.0.
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>
- 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>
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>
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>
Overhaul of the IP info card:
Layout
- Card grid hard-pinned to 2-up on PC (`md:grid-cols-2`); card count options
become 2 / 4 / 6 (all even, last row always fills).
- Default ipCardsToShow drops from 6 to 2.
- Hero IP font size unified via a shared helper — full-form IPv6 only tiers
down on mobile; at md+ IPv4 and IPv6 cards match.
- Metadata grid: 2 cols on mobile, 3 cols on PC; ISP takes its own row
(col-span-3) so long provider names aren't crammed into a narrow cell.
- Advanced block (signed-in): 2×2 vertical label-on-top grid, matching the
metadata rhythm; locked preview gets the same treatment.
Map on demand (replaces the always-inline banner)
- Map button lives next to the City value (semantically where the location
sits), not in the Hero IP row.
- Click opens a centered Dialog with a larger image + location caption.
- `showMap` preference, `prefShowMap`/`toggleMaps` functions, `m` shortcut,
and their i18n keys (`nav.preferences.showMap*`, `Tooltips.ToggleMaps`,
`shortcutKeys.ToggleMaps`) are all removed. A new `Tooltips.ViewOnMap`
key replaces them across en / zh / fr / tr.
Shared IpDetailPanel
- New `frontend/components/ip-infos/IpDetailPanel.vue` owns everything below
the Hero IP — metadata grid, advanced block (signed-in + locked), ASN row
with Collapsible ASNInfo, and the optional Map Dialog. Multi-root template
on purpose so ASN's `mt-auto` still bottoms-out in the card.
- New `frontend/utils/hero-ip-size.js` centralizes the Hero IP font-size
tiers; both IPCard and QueryIP import it.
- IPCard shrinks to header + Hero IP (with Copy) + `<IpDetailPanel
enable-map>`; QueryIP shrinks to input + Hero IP (no Copy) +
`<IpDetailPanel>` (enableMap stays false to avoid Dialog-in-Dialog).
- As a side-benefit QueryIP picks up ASN expansion and the redesigned
metadata / advanced / locked rhythms for free.
Test updates
- `tests/default-preferences.test.js`: drop `showMap`, set `ipCardsToShow: 2`.
- `tests/composable-shortcuts.test.js`: drop the `toggleMaps` mock + its
counter; baseline shortcut count 27 → 26.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spinner is shadcn-vue's canonical "loading spin" primitive: lucide Loader2 +
animate-spin + role="status" aria-label. Button cva's [&_svg]:size-4
[&_svg]:shrink-0 ensures that slotting it into a Button auto-aligns.
QueryIP submit button: when isChecking === 'running', prepend <Spinner />
and disable — lets the user see the system is working while blocking
duplicate clicks.
— Convention recorded (followed by all future "kicks off a request" buttons):
1. Color uses variant="action" (blue-500 / dark blue-400)
2. Loading tier prepends <Spinner /> + :disabled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InputGroup owns border / rounded / shadow / focus-within:ring at the
wrapper level; inside, the [&>input] / [&>button] permeating selectors
flatten child elements' independent border / shadow / ring / rounded.
No more hand-written rounded-r-none / rounded-l-none / -ml-px border-
collapse hacks.
QueryIP is the first beneficiary; 5 more Input+Button stitching sites
in advanced-tools/ (DnsResolver / CensorshipCheck / Whois / GlobalLatencyTest)
will migrate later together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuse IPCard's visual language: "hero IP + 2-col metadata dl + advanced
data + ASN":
- Hero IP row + fitOneLineClass font-size degradation
(≤15 text-2xl / ≤26 text-xl / >26 text-base)
- Metadata dl (Country/Region/City/ISP); Country uses circle-flags,
replacing .fi fi-*
- Advanced data (only when ipGeoSource===0) follows the IPCard rules:
· Type / Proxy / Native as plain text, no color judgment (don't label
"being a proxy / non-native" as good/bad)
· Quality Score keeps the semantic-color Progress (objective score,
coloring is appropriate)
· Not-signed-in tier uses the same Lock CTA + 2×2 field menu +
t('ipInfos.advancedUnlockCta')
- ASN row has a bgp.tools external link
Others:
- Header uses DialogHeader, no more hand-written
- Float button bg-blue-600 hard-coded → Button variant="default" (tracks theme)
- Input + Button composed with -ml-px border-collapse
- Clears 1 out of the 13 .fi fi-* flag migration tasks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The progress bar and connection-info row both used v-if conditional
rendering: they didn't occupy space in the idle tier, then appeared
abruptly when running. The result was the metrics below jumped downward
when you clicked Start.
Fix: always reserve space for both elements:
- Progress bar: always rendered, idle tier wears invisible class (keeps
h-2 + mb-6 layout space; just hides the visual). Becomes visible when
running/pause/finish.
- Connection info row: wrapper always renders with min-h-6 + mb-3; the
inner content uses a Vue Transition to fade/slide in. Outer space is
stable, the content alone animates.
Smoother start interaction; no layout shift.
- Floating queryip button: was .btn.btn-primary with fixed-position
custom CSS. Now <Button size=icon> with blue overlay; right-edge
alignment for >1600px screens via reactive :style (replaces
document.querySelector('.queryip')).
- Dialog internals rewritten as plain Tailwind structure (no more
modal-content/header/body/dark-mode-border).
- Input field: .form-control → shadcn <Input> from new
components/ui/input/ (thin wrapper around native <input> with
shadcn's default border/focus ring).
- Submit button: conditional btn-primary/btn-secondary → <Button>
with :class swapping bg-blue-600/bg-neutral-500.
- Button + Input joined look (was .input-group): rounded-r-none on
the input, rounded-l-none -ml-px on the button.
- Result list: .list-group + .list-group-item jn-list-group-item
+ .col-auto/.col-10/.col-8 → plain <ul> + per-item flex with
shrink-0 label and flex-1 value. Every row has a bottom border
via Tailwind border-b.
- Quality score bar: .progress + .progress-bar with manual
:style width → shadcn <Progress :model-value> from new
components/ui/progress/ (wraps reka-ui ProgressRoot/Indicator).
- ASN link: .link-underline-opacity-* + link-light/dark → plain
no-underline hover:underline + dark:text-* text-*.
Verified: npm run check — build + 46 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous self-hosted v-tooltip directive (frontend/directives/tooltip.js)
did not actually render in the page — consistent with the project's
long-standing observation that Bootstrap Tooltip also misbehaved here, which
was why main.js had the original custom registration block. Replace the
directive approach entirely with shadcn-vue components.
New UI components (frontend/components/ui/tooltip/):
- Tooltip.vue — wraps reka-ui TooltipRoot
- TooltipTrigger.vue — wraps TooltipTrigger (as-child by default)
- TooltipContent.vue — wraps TooltipContent + TooltipPortal, styled
with bg-foreground / text-background
- TooltipProvider.vue — wraps TooltipProvider (delay 150ms)
- JnTooltip.vue — convenience wrapper so call sites can write
<JnTooltip :text :side>…</JnTooltip> instead
of nesting four components. Automatically
renders children without a tooltip when
store.isMobile is true (preserves legacy
behavior: no hover tooltips on touch devices).
App-level wiring:
- App.vue wraps the whole template in <TooltipProvider> so all descendant
JnTooltip instances share one provider and inherit its delay settings.
- main.js: remove the tooltip directive import and registration; delete
frontend/directives/ entirely.
Call-site migration (11 sites, all preserve original placement):
- components/WebRtcTest.vue, ConnectivityTest.vue, DnsLeaksTest.vue,
SpeedTest.vue, Footer.vue, ip-infos/IPCard.vue (×4),
widgets/InfoMask.vue, widgets/QueryIP.vue
Plan update:
- refactor/01 phase B tooltip entry rewritten to explain both the failed
first attempt (self-hosted directive) and the successful shadcn path.
- refactor/01 phase C Collapse section records a known issue flagged by
the user during phase B verification: after expand, Bootstrap Collapse
content goes blank. Root cause likely interaction with Tailwind
Preflight / tw-animate-css / state-attribute selectors. Deferred to
phase C where Collapse is replaced with shadcn-vue Collapsible anyway.
Verified: npm run check — 23 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete phase B of refactor/01 by removing the last direct imports
of Bootstrap JS (Modal, Tooltip) from component code. Four Modals and
the v-tooltip directive now use shadcn-vue / a lightweight replacement,
matching the Toast and Offcanvas work that already landed.
Tooltip — self-hosted replacement for Bootstrap Tooltip:
- Add frontend/directives/tooltip.js — minimal teleport-to-body tooltip
with hover/focus show, blur/mouseleave hide, placement top/bottom/
left/right, viewport clamping, mobile skip (matches legacy behavior)
- main.js: remove `import { Tooltip } from 'bootstrap'` and the inline
app.directive('tooltip', ...) block; register the new directive
- All 11 v-tooltip call sites across the codebase are untouched — the
binding API (string | {title, placement}) is preserved
Modals — 4 components migrated to shadcn-vue Dialog:
- Add frontend/components/ui/dialog/ (Dialog, DialogContent, DialogClose)
- components/widgets/Help.vue — local isOpen ref, openModal() preserved
- components/widgets/QueryIP.vue — local isOpen ref + focus trap on
input when opened (replaces old shown.bs.modal listener)
- components/User.vue — local isOpen ref
- components/Additional.vue — local isOpen ref
- Drop all `import { Modal } from 'bootstrap'` and data-bs-toggle/dismiss
main.js — import 'bootstrap' kept (deferred to phase C):
- 4 remaining Bootstrap JS widgets (Dropdown, Collapse, Tab, ScrollSpy)
are still driven by data-bs-toggle attributes in Nav.vue, DnsResolver,
Whois, SecurityChecklist, MtrTest, IPCard, Achievements, App.vue.
Full removal of the bootstrap bundle is deferred to phase C where the
visual layer rewrite will replace those with shadcn-vue equivalents.
- refactor/01 plan updated: phase B checklist ticked; phase C gets an
explicit "prerequisites" block listing the 4 widget families to migrate
so the bootstrap import can finally go away.
Zero remaining `import ... from 'bootstrap'` in component code.
Only main.js retains the global `import 'bootstrap'` side-effect for
the 4 widget families above.
Verified: npm run check — 23 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>