IpDetailPanel had grown to ~750 lines holding three independent regions.
It is now the shell (metadata grid, inline Map / Country-traffic panels,
dialogs) with unchanged props, emits and multi-root template, rendering
IpAdvancedBlock (with ScoreDetailsPanel beside ScenarioFrictionPanel) and
IpAsnBlock, which relay view-usage / open-tool through it.
The inline /api/asn-history fetch becomes loadAsnHistoryInto in
utils/ip/asn-history.js, matching the other ASN loaders, now with tests.
Markup, copy, analytics and per-IP reset behaviour are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The advanced block gains two signed-in fields under the existing four:
Abuse activity (the reputation level) and Scenario friction, ten usage
scenarios each rated smooth / extra checks / may be restricted. The card
shows one tone dot per scenario instead of a score, so it can't be read
as a second quality score; the details list follows the score details'
layout and closes with a Persona Check invitation.
Green and yellow only (a new light-yellow ok-mild tone for extra checks),
no reasons shown, copy in every full locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Raycast / curl / Lite badges sat in an unlabelled image row of their
own under the Advanced Tools. The footer now has four columns aligned
with that card grid — brand, Project, Docs & Support, Clients — so the
product entries get a heading and show on every route; the curl
commands keep their dialog. Docs links share one locale-aware helper.
The footer never repeats on-page content, and the fixed footer height
in style.css goes, as the columns size themselves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A blocker setup is part of the visitor's own environment, like the
browser info and proxy-rule sections, and is worth handing to whoever
helps debug it. The section stays deliberately small: request counts per
category (blocked / not blocked / no answer / total), no per-address or
per-vendor rows, so a report stays readable and the list can change
without breaking old links. Group ids are capped strings rather than an
enum for the same reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
People running uBlock Origin, Privacy Badger, a blocking browser, or a
DNS / VPN / router filter had no way here to see what their setup lets
through. The new tool probes a static list of widely used ad, analytics,
social, error-monitoring and device-maker endpoints plus popular tracking
scripts (fetched with no-cors and no cookies or referrer, never run), and
places bait elements for cosmetic filters. Nothing runs until the visitor
starts it; every group starts selected.
A timeout reads as "no answer" and stays out of the score, and every
listed host was checked to resolve, serve valid HTTPS and not redirect to
http:// or in a loop, since any of those would fail like a block. Element
hiding is left out of the score (DNS-level filters can never pass it) and
instead tells browser-level from network-level blocking. The page copy
says outright that blocking everything is not automatically better.
Ships with the x shortcut, locale copy for every full locale and a
changelog entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runChain's optional originalSite / accept move into an options object, so a
chain that needs only one no longer passes an `undefined` placeholder.
The per-version validators become isValidIPv4 / isValidIPv6 in
common/valid-ip.js, next to isValidIP: the "Valid" prefix keeps them apart
from the non-validating isIPv6 discriminator.
Tests now run the four single-version entry points against a stubbed fetch,
so dropping or swapping a chain's validator fails the suite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VITE_FIREBASE_AUTH_DOMAIN is each environment's own host, served by the
/__/auth proxy; the PWA-only VITE_FIREBASE_PWA_AUTH_DOMAIN and
utils/auth-domain.js go. Every OAuth provider lists the handler URL per
environment (Google and GitHub both take several redirect URIs).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
GitHub OAuth apps now take several redirect URIs, with optional
wildcard matching, so the PWA's own auth domain can be registered
there like Google's; the menu no longer hides GitHub when the PWA
signs in through a different domain (signInProvidersFor and the
store getter go). The operator notes name both registrations.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
A pass over every file the branch touched, after the iOS animation hunt
left tries and reverts behind it. Removed what nothing reaches: the
view-transition rule for a transition no code starts, an unreachable
guard in the transition hook, a no-op KeepAlive key branch, a sign-in
guard the menu already enforces, usage-dialog wrappers that only
forwarded to the store, a duplicated modifier-click check and card
filter (isPlainClick / listedTools), a test that hand-rolled
listedTools, a drawer-era fixture parameter. auth-flow.js folded into
auth-redirect.js. Comments cut to what the code cannot say itself —
file headers of a few lines, the measured iOS reasons, non-obvious
orderings — and stale ones fixed (Nav under KeepAlive, Home.vue
reusing DEFAULT_META, "from Nav", enabledCards). AGENTS.md sections
tightened to the code as it is now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
On iOS, accelerated CSS animations running before a swipe-back stay
visually frozen once the gesture lands on the previous page: the frame
rate, the animations' own time and their iteration events all carry on,
but the compositor paints no new frame for them until their layer is
rebuilt (measured with the ?debug=nav overlay; cancel + play, a
display:none round trip or a re-insert revive them, ancestor style
changes do not). A push revives them as a side effect of its page
transition; a history step, where the browser's gesture replaces our
transition, does not. After every navigation that came from popstate,
utils/revive-animations.js cancels, replays and resets to its time each
running CSS animation, two frames after the DOM lands and again at
400ms and 1200ms in case the gesture snapshot is dismissed late.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
Route changes animate like native navigation: a tool page slides in
from the right, going home slides it back out, tool to tool cross-fades
— utils/page-transition.js decides the direction from the route pair
and wraps the router's DOM update in document.startViewTransition from
router/index.js, so the snapshot, the swap and the scroll restoration
happen before the animation and pages of different heights never
overlap. Nothing animates on a query-only change, under reduced motion,
on a history step the browser already animated (swipe-back), or in a
browser without the API; a navigation arriving mid-transition skips the
running one. The Nav opts out by view-transition-name and stays put
while the page moves.
Reapplied after the Nav's animation stall on iOS proved unrelated: it
reproduces with the transitions removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
Reverts the View Transitions hook-up, its keyframes, the Nav naming
and the two diagnostic follow-ups. On iOS the Nav's own animations
(Pulse's globe and ping) stalled after the forward transition and
stayed stalled after a swipe-back, while a transition back home
restored them — the capture of the fixed Nav is the suspect, and the
clean way to confirm it on the device is to run without any of it.
This reverts commits 2a360f0, d6d80da, 38a61fd, 156d0bf and 856f965.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
`?vt=off` turns the View Transitions off, `?vt=unnamed` keeps them but
leaves the Nav un-named (it slides with the page), `?vt=default`
restores the normal behaviour; the choice lives in localStorage. WebKit
cannot run in the sandbox and the Nav's animations stall on iOS after
a transition, so this is how the trigger gets bisected on the device.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
WebKit leaves the accelerated CSS animations inside an element captured
under its own view-transition-name stalled once the transition is over:
after the first page transition Pulse's spinning globe and ping only
repainted in half-second jumps (measured frame by frame on iOS 26), and
a swipe back brought the stalled Nav home. Every transition now ends by
cancelling and replaying the running animations in the named elements
(restartAnimations in utils/page-transition.js), which recreates them
on the compositor.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
VITE_FIREBASE_AUTH_DOMAIN stays the shared sign-in domain every browser
uses; the new optional VITE_FIREBASE_PWA_AUTH_DOMAIN is the domain the
installed PWA initialises Firebase with — the site's own host, served
by the /__/auth proxy, so the redirect handler runs inside the PWA
(utils/auth-domain.js). GitHub's OAuth app has one callback, on the
shared domain, so when the PWA domain differs the account menu offers
Google only. .env.example documents both and the console-side steps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
Route changes animate like native navigation: a tool page slides in
from the right, going home slides it back out, tool to tool cross-fades
— utils/page-transition.js decides the direction from the route pair
and wraps the router's DOM update in document.startViewTransition from
router/index.js, so the snapshot, the swap and the scroll restoration
happen before the animation and pages of different heights never
overlap. Nothing animates on a query-only change, under reduced motion,
on a history step the browser already animated (swipe-back), or in a
browser without the API; a navigation arriving mid-transition skips the
running one. The Nav and the iOS status-bar tint opt out by
view-transition-name and stay put while the page moves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
The w / d / s / c keys scrolled a section 80px below the viewport top
while the nav links used 70px, so the same section landed at two
different spots. SECTION_SCROLL_OFFSET now lives in utils/scroll-to.js
and every section scroll — nav, breadcrumb, keyboard — uses it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
An iOS standalone PWA has no popup with a usable opener, so
signInWithPopup never delivered its result there: the provider sent
the visitor back to the homepage and the app saw no sign-in. The store
now picks the flow (utils/auth-flow.js): popup in a browser tab, as
before; signInWithRedirect in the installed PWA, and as the fallback
when a popup is blocked. A redirect marks its provider in sessionStorage
(utils/auth-redirect.js) and boot consumes the marker before first
render with getRedirectResult, so the first authenticatedFetch carries
the token like the signed-in boot path. The toast host now shows an
alert raised before mount, which a failed redirect return is.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
The homepage sections and the advanced tools are peers, so every route
shows the same Nav, rendered once from App.vue alongside the User and
Preferences hosts. What differs is the click: on the homepage a section
link scrolls; elsewhere it navigates home and then scrolls, re-aiming
while the freshly mounted homepage still resizes; an advanced tool
entry opens /tools/<slug> from anywhere (utils/nav-target.js +
composables/use-nav-target.js). The slim page header, its tools menu
and the body padding switch it needed are gone; open sheets close on a
route change.
Tool pages, /privacy and /r/:id carry a breadcrumb above their title
(PageBreadcrumb.vue): Home steps back in history when the homepage is
the previous entry, Advanced Tools goes home to the cards, the current
page is the last crumb. Cards use an arrow instead of the drawer icon.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
Every way into a tool — the homepage cards, the nav dropdown and mobile
sheet, the keyboard shortcuts, ASN Info's profile link, the banners —
navigates to /tools/<slug>; the bottom drawer, its full-screen toggle
and the ?tool= query it was driven by are gone from Advanced.vue. The
pages stay alive (App.vue / ToolPage.vue), so browser back and forward
move between tools and the homepage, each restored as it was left.
Legacy /?tool=<slug> links redirect to the page in a router beforeEach
(utils/legacy-tool-link.js: known slug → its page with the remaining
query, unknown → the homepage). Persona Check drops noStandalone: with
the homepage kept alive its dependency commands stay registered, so the
page dispatches them in place and only a direct visit navigates home
first. The four sign-in tools no longer close a drawer before opening
the usage dialog, which PageHeader's User hosts.
Changelog entries in the six full locales; frontend/AGENTS.md describes
the page model.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
The pages outside the homepage (/tools/:slug, /privacy, /r/:id) get one
PageHeader: a back button that steps back in history when the previous
entry is the homepage (restoring the kept-alive page and its scroll) and
pushes / otherwise (utils/back-target.js), the brand, a tools menu
grouped by category (ToolsMenu.vue, over listedTools() in
utils/tool-availability.js) so a tool can be switched without going
home, the current page's title, and the account menu. Below `sm` the
row keeps the back arrow, the title and icon triggers.
The account menu is UserMenu.vue, extracted from Nav.vue, which keeps
its look; the header also hosts User and Preferences so sign-in,
benefits & usage and the preferences sheet work on every page. Each
Preferences instance opens only on the page on screen and closes when
its page is left; My Achievements off the homepage navigates home first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
App.vue wraps the router view in a KeepAlive holding Home and the tool
page, so leaving for /tools/:slug and coming back neither re-runs the
homepage tests nor loses the scroll position; ToolPage (StandaloneTool
renamed) reads its slug reactively — the same instance now switches tool
on in-app navigation — and caches each visited tool per slug in its own
KeepAlive (max 8), dropping them all when the signed-in account goes
away (utils/tool-cache.js).
Cached pages pause while hidden: composables/use-route-active.js gives
useRouteActive / useActiveValue / useActiveEventListener, and the head
writer, the shortcut map, the scroll trackers, the ?q= watchers of ASN
Profile and IP Calculator, the User dialog triggers and the overlay
counter all act only for the page on screen. An overlay still open when
its page is left closes, as its portal lives outside the cached page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
A new In-depth Checks tool shows which blocklists an IP is on: the
real-time DNSBLs and the threat intelligence lists the private API's
/iphitlist checks. Listings are split by kind, so a VPN or Tor exit
reads as an anonymity mark rather than a bad record; hits stay open with
their reasons and removal links, the remaining lists fold away, and a
legend explains every mark.
/api/ipblocklist proxies the upstream with the caller's headers and a
30s timeout (a cold DNSBL pass outlasts the default 8s), passing sign-in
and quota rejections through. Signed-in users only, original site only,
150 checks a month (ip_hitlist in the usage dialog and the pre-flight
quota gate). Shift+B opens it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
MAC Lookup no longer calls maclookup.app. The five public IEEE registries
(MA-L, MA-M, MA-S, IAB, CID) are a new `oui` dataset row — no key needed,
published as downloaded, refused below per-registry row floors — and
common/oui-db.js answers the longest assignment covering a query, the group
bit masked so a multicast address resolves to the block it derives from.
The country is read off the end of the registered address.
?mac= now takes a vendor prefix as well as a full address (6–12 hex digits,
colon / hyphen / dotted / bare), parsed by common/mac-input.js, which the
form shares. An address no block covers is still answered, with its flags.
The route is gated by requireOfflineData while the registries download at
boot. MAC_LOOKUP_API_KEY is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend downloaded every missing dataset before listening, so a first
boot (or a newly added dataset) refused every request for a minute or more.
Snapshots already on disk now load before the listener opens and missing
ones download behind it. Meanwhile only routes reading still-missing data
answer 503 (never edge-cached): /api/maxmind, /api/asn-connectivity and
/api/service-status. An ASN Profile section whose data is still loading
answers 'error' on its own, and the connectivity graph client doesn't cache
a 503, so both recover once the data lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildCountryShares now reports whether its walk completed: a lookup with
no answer (no database, or a failed lookup) and the lookup budget, now a
hard cap across blocks, both mark it incomplete, and the bgp-prefixes view
serves such an answer uncached. A prefix-less AS needs no lookups, so it
stays cacheable even without MaxMind. ASN_PROFILE_VERSION goes to 6 so
profiles cached with first-address country shares are not served on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the ASN Profile work, mostly one class of bug: a failed
upstream read as "no data", which the 7-day edge cache would then pin.
- AS Rank: a 200 carrying GraphQL errors is a failure, not an unknown AS.
- Radar: an answer built from partly failed segments is still shown but
flagged `incomplete` and kept out of the cache, on the aggregate and on
the standalone asn view. A 404 is Radar's "no such AS" and counts as
empty, as does a segment that simply has no figures.
- Reputation: a malformed or non-200 answer is an error.
- Prefixes: a payload without `prefix_origins` is an error; [] stays valid.
- Radar is `empty` when only local CAIDA counts are present.
- requireValidASN enforces AS1–AS4294967295 and canonicalizes; the range
rule lives once in common/asn-input.js, shared with the frontend.
- The PeeringDB listing lookup goes through fetchUpstream.
- New functions follow the const-arrow convention; test stubs compare
parsed hostnames instead of URL substrings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A third offline dataset joins the CAIDA updater: the daily PeeringDB dump,
distilled by a streaming pass into a compact per-ASN index (network type,
scope, peering policy, exchange and facility presence). Contact tables are
never parsed and the raw dump is deleted after distilling. It downloads
only when the Cloudflare key is configured, since the tool is hidden
without it.
The profile shows network type, scope and website in the header and a
"Peering & facilities" card whenever the AS lists an exchange or facility.
Aggregate deadlines now sit above the fetch timeouts they wrap, so a
source that degrades internally still delivers its partial answer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the v7.7.0 changelog line for the new tool, drops two exports
nothing imports, and aligns the reputation test fixtures with the fields
the private API actually returns.
Co-Authored-By: Claude Fable 5.1 <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>
The Whois tool and /api/whois now take `AS<n>` (prefix in any case) next
to IPs and domains, answered from the RIR's RDAP autnum record. A bare
number stays invalid input, so it can never be mistaken for anything else.
The raw blocks no longer start with the template's own indentation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follows the v3 score output of MyIP-api (this frontend no longer reads
the v2 shape; deploy it right after the API).
- A toggle next to the quality score opens a panel: a short explanation
assembled from the score dimensions (network type, anonymity with its
last-observed date, reputation, the ASN's neighbourhood, the verdict),
and all boolean attributes as check / cross / question-mark icons.
Residential proxies seen only in the API's own sighting history read
as "suspected".
- The card's "Proxy" field becomes "Anonymizer": Tor exit, iCloud
Private Relay, residential proxy, proxy, VPN (each with a suspected
variant when only one source agrees) or not detected. Card fields are
anonymity / anonymityCode / anonymityProvider / anonymityProtocol; the
provider line hides when the API returns null. Texts live under
ipInfos.anonymity.*, with one code-to-key map for card and report.
- Diagnostic reports store the anonymity code (REPORT_VERSION 2; v1
links show the unsupported-version page, reworded to fit either
direction). The persona collector no longer sends the unused isProxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ninety days is longer than most visitors need for a local record. Stored
preferences above the new cap clamp to 30 on read, and older day buckets
are pruned on the next load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collapse repeated daily IP records into summaries ordered by last seen.
Keep country and IP-type filters across both views and expand dates only
for IPs recorded on multiple days. Preserve the daily storage format.
Co-Authored-By: GPT-6 <noreply@openai.com>
Avoid Number overflow when parsing long standalone or dotted octal
input. Cover oversized values, leading zeros, and IPv4 boundaries.
Co-Authored-By: GPT-6 <noreply@openai.com>
Show a fixed preliminary test before the selected measurements and keep
formal results independent. Start testing without waiting for charts or
connection metadata, buffering samples until charts are ready.
Add localized phase labels and immediate startup progress, reduce the
formal latency sample count to 20, and document the improvement.
Validation: pnpm check (1533 tests passed and build succeeded).
Co-Authored-By: GPT-6 <noreply@openai.com>