The layout refactor moved the two re-export bridges into utils/ip/ and
utils/features/; their header and test comments still named the old
paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fifteen cards in one flat grid mixed network diagnostics, lookups and the
in-depth checks that only run on IPCheck.ing. Each registry entry now
carries a category, and Advanced.vue renders the cards under three
headed groups: Network Diagnostics, Lookup & Calculation and In-depth
Checks. A group whose tools all fail their gates drops out with its
heading, so a self-hosted deployment simply shows no In-depth Checks.
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>
Returning early on empty input left the previous result and error on
screen. It now clears them first, and the button stays disabled for a
whitespace-only input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skipping a row while this process published it also dropped a change
someone else made in that window: no file moved afterwards, so no later
check came. The watcher now looks again once the publish has recorded its
files, and reloads if they are no longer its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watcher told its own publish apart by the fingerprint reloadRow
records, but that lands only after the reload finishes. A settle tick in
between — the state write and a slow reload outlast the settle window, as
on a loaded CI runner — saw new files under an old fingerprint and reloaded
them a second time as a "file change". Rows being published by this
process are now skipped by the watcher until that reload has recorded them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared parser rejects an empty string, so Enter in the empty field
showed "Invalid physical address". onSubmit now returns early on empty
input, as IP Calculator does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isRand marked every locally administered unicast address, CID ranges
included. A CID is assigned local space (IEEE 802c ELI), so an address a
registered CID covers is no longer reported as random. For the rest the
U/L bit stays the signal: it is the only mark randomization leaves on an
address, and what the previous provider reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The success test read the checkout's downloaded registries and fell back
to asserting a 503 without them, so CI never exercised a 200. The handler
now comes from createMacChecker(lookup); the tests pass a fixture lookup
for the 200 and a null one for the 503.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three review findings in the IEEE registry reader:
- A country code after the postal code now wins over a state code before
it ("Dover DE 19901 US" → US), except after NL / IT, whose postcodes are
followed by letters (Dutch letter pairs, Italian provinces). Over the
live registries this corrects two addresses and changes no other.
- A registry file that stops mid-row, or inside a quoted field, is refused
as truncated; IEEE ends every row, the last included, with CRLF.
- A multicast query shows its block's multicast range (01:00:5E:…), which
holds the address asked about, instead of the unicast registration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form validates with the shared common/mac-input.js parser, so vendor
prefixes go through. A collapsed "See examples" fold under the input, as in
IP Calculator, holds one pill per kind of answer: full address, vendor
prefix, MA-M block, private registration, multicast and randomized. Browsers
expose no MAC address, so there is no "my address" pill. Changelog entry in
every full locale.
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>
A scheduled or catch-up run that found every dataset current logged
nothing — only a publish wrote a line — so a run that fired looked like one
that never did. updateDatasets now logs the run's start and, per row, that
it was up to date or skipped for another process's lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CauPFca6bYL9qpuYEmsU9H
updateDatasets applies one timeout to every row, so the 50 ms that stops
the stalled row also bounded the healthy one — a lock, a temp dir and two
fsync'd state writes — and a loaded CI runner timed it out too. 1 s keeps
the stall test meaningful with room for the healthy row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CauPFca6bYL9qpuYEmsU9H
- A run that died or lost its lock between renames left every file present
(possibly a mixed MaxMind City / ASN pair), and the boot download skipped
the row because the files existed. The in-flux state now carries
`publishing: true`, cleared by the final record, and boot re-fetches a row
whose last publish never finished.
- The boot skip asked whether a snapshot file existed, so an empty or
corrupt one — which the reader refuses — kept the row unrepaired. Rows now
supply isLoaded(), the reader's own readiness (all four), and boot
downloads whenever the reader holds nothing usable; files under the
published names are re-fetched even when the state records the remote's
version (updateDataset's new `force`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CauPFca6bYL9qpuYEmsU9H
publish took the combined signal (caller timeout + lock loss) and treated
any abort as a lost lock, so a scheduled or boot timeout firing between
renames skipped the rollback and cleanup while this process still held the
lock, leaving a mixed set and .next / .bak files. The combined signal still
cancels, but only the lock's own signal now stops rollback and cleanup.
Once every rename is done, only a lost lock keeps the version from being
recorded, so a timeout landing after a complete publish no longer forces a
re-download.
The lock-loss test now trips the engine's real onCompromised handler; a
new test covers the timeout path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CauPFca6bYL9qpuYEmsU9H
- The watcher started only after every boot step, so a dataset published
by another process (or placed by hand) while another step was running
became the watcher's baseline and was never loaded. It now starts before
the boot downloads.
- The CAIDA readers serve the newest *.txt under any name, but the boot
download checked only the published names, so an upgrade with a
hand-named snapshot re-downloaded it. A row can now supply hasSnapshot();
as2org / as-rel use their reader's own rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows update one after another, each with up to 30 minutes, so four datasets
can legitimately take two hours while the monitor failed a run after one.
maxRuntime is now every scheduled row's timeout plus a 10-minute margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rolling back after a lost lock could overwrite what the lock's new holder
had just published. Once the lock is lost, publish now touches nothing more
(no rollback, no cleanup). To keep a half-done set from passing for
current, the state names no version from the start of publishing until the
final write, so whoever runs next — after a lost lock, a crash or a kill —
re-fetches and publishes a whole set. A rename failure with the lock still
held is rolled back as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With connectivity now an error when no as-rel snapshot is loaded, the
network-down handler test has no working source on a data-less checkout
(CI), so the handler rightly answers 502. The test asserts that there and
keeps its 200 expectations where a snapshot exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock-loss signal was checked only before publishing began, so a lock
compromised mid-copy or mid-rename let publication continue. It is now
checked before every copy and rename, an abort taking the same rollback,
and again before the state is written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PeeringDB is 'disabled' only without a Cloudflare key; configured but not
loaded (still downloading, or the download failed) is an 'error', so the
ASN Profile isn't cached for a week without it.
- Connectivity without an as-rel snapshot is an 'error' too, not an empty
graph; a missing as2org still only blocks while it downloads, as RIPEstat
names the nodes.
- A CAIDA as2org / as-rel file that parses to nothing (corrupt) no longer
counts as loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A compromised lock (failed heartbeat; another process may take it over)
aborts the update holding it, download included, before it publishes,
instead of only being logged.
- A failed publish removes files it had just added when the dataset had none
before, as well as restoring replaced ones from backup: never half a set.
- The watcher reloads hand-placed or externally published files only once
they have held still for the settle interval and passed the row's
validator, so a file still being copied never replaces healthy data.
- Back to write-file-atomic 8, with engines aligned to its range
(^24.15.0 || >=26.0.0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Scheduled and catch-up runs give each dataset 30 minutes, download
included, so a stalled upstream can't hold up the rest or, under croner's
overrun protection, every later run.
- Downloads go through fetchUpstream (project User-Agent, a timeout on the
wait for a response); the body stays bounded by the caller's signal.
- Catch-up asks whether a scheduled run fell between the last check and now,
instead of a 25-hour age that missed a check skipped just after one.
- A remote that names no version (no Last-Modified) is always fetched, not
taken for unchanged; maxmindIdentifier is null when an edition omits it.
- The watcher records a dataset's files as loaded only after the reload
succeeds, so a failed load is retried on their next change.
- write-file-atomic 7, which supports the project's Node >=24 (8 needs 24.15).
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>
MaxMind and CAIDA each had their own updater with its own lock, state file,
publish step, boot download and timer. One engine (common/dataset-updater.js)
now owns all of that, and each dataset is a row in common/datasets.js that
holds only its own logic: find the newest remote version, fetch or distill
it, validate, reload.
- Locks: proper-lockfile, a heartbeat lock shared by every process, so a
crashed holder's lock goes stale on its own; a booting server waits for a
live holder and loads what it published.
- State: one file per dataset, written with write-file-atomic; the old
.maxmind-update-state.json / .caida-update-state.json are read once, so an
upgrade doesn't re-download.
- Schedule: croner at a fixed time (DATASET_UPDATE_CRON, default 04:30 local)
instead of 24 h from process start, plus a catch-up run at boot for a check
a downtime skipped. DATASET_AUTO_UPDATE (default on) replaces
MAXMIND_AUTO_UPDATE / CAIDA_AUTO_UPDATE, which still apply when it is unset.
- Reload: the engine watches every dataset's files, so a publish by another
process or a hand-placed .mmdb is picked up; MaxMind's own watcher goes.
MaxMind's City and ASN editions now publish together under one version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shares over the part of the space walked before the budget ran out would
read as the ASN's whole IPv4 distribution. An exhausted budget now gives no
shares, like a lookup without an answer, and the answer stays uncached.
Real ASNs use at most ~30k of the 100k lookups.
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>
The profile read `q` only on mount, so a new ASN link or history step with
the drawer open changed the URL but not the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MaxMind unavailable (a failed download) the bgp-prefixes view answered
`countries: []` as complete, and it was edge-cached for a week. The answer
is now marked partial, and the ASN Profile's prefixes section incomplete,
so neither is cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Country shares credited a whole announced prefix to the country of its
first address, so a /8 spanning many countries counted as one. Each prefix
is now walked one MaxMind network at a time, within a per-request lookup
budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's IPv4/IPv6 prefix counts came from Radar's routes/stats, an
endpoint that fails often and, as one of the profile's summary segments,
kept complete ASN Profiles out of the edge cache. The ASN Profile already
shows prefix counts (from the pfx2as list) and relationships (the CAIDA
graph), so the panel drops both: no more routes/stats call, and the Radar
asn view no longer reads the local CAIDA snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pino-pretty transport runs in a worker thread. In a loaded parallel
run, a spec that logs shortly before finishing could occasionally be kept
alive by that thread and never exit, stalling the whole suite until the
CI timeout. `pnpm test` now preloads tests/setup.js, which switches the
logger to its synchronous JSON output unless LOG_FORMAT is already set.
Runtime logging is unchanged.
Co-Authored-By: Claude Fable 5.1 <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>
The tool is titled as a lookup, its card note is one short line, and the
intro names PeeringDB among its sources — across all six locales.
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>