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
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
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>
sticky top-0 resolves against the scroll container's content box and
never covers the container's own padding, so the results-state layout
(where the padded wrapper itself became the scroll container) pushed
the tally down by py-3. Split overflow and padding into separate
layers so the scroll container is always padding-free, in all three
Globalping-backed tools.
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>
New GlobalpingCountryPicker renders the caller's suggestion sections plus
the full per-continent catalog of probe-having countries (from the backend
inventory), all over one v-model selection with section-level select-all
and a sticky tally. Caps are soft: past the limit (30 for MTR/latency, 12
for censorship) the tally turns red and the run button locks. MTR and
Global Latency keep their curated 28-country spread as the default-selected
suggestion; CensorshipCheck migrates its inline picker to the component and
gains the free catalog. Left picker column matches the results column's
height once results exist (absolute-fill trick, min-height floor).
Also folds CensorshipCheck's render-function table back into the template.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every "my network" test now emits a <domain>:finished event with its
complete structured result on the app bus (existing thin events keep
their fields — achievement rules are untouched). Connectivity and
WebRTC additionally record locale-free status/NAT enums next to their
localized labels so downstream consumers never parse t() output.
A collector mounted once in App.vue normalizes each payload through
per-section builders (shape-based cleaning, no free text) and keeps
the latest schema-valid snapshot per section for the share dialog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raw mtr text was hard to scan and useless on mobile. Parse each probe's
output (header-driven column mapping, tolerant of differing mtr builds)
and render a compact table: adaptive Host/IP/ASN identity columns plus
whatever numeric columns the data carries; falls back to the raw text
when nothing is parseable. Table headers localized in all four locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advanced tools can now be opened two ways: the existing in-page drawer, and
a real standalone page at /tools/:slug (shareable, crawlable, new-tab-able).
Same tool components render in both; only the wrapper differs.
- Router moves to HTML5 history mode: `/` → Home.vue, `/tools/:slug` →
StandaloneTool.vue. The drawer is now driven by a `?tool=<slug>` query on the
home route instead of a nested route, so ⌘/Ctrl-click, middle-click and the
drawer-header ↗ open the standalone page.
- App.vue is now a thin shell (tooltip / toast / PWA / theme + <router-view>);
the homepage body moved to the new Home.vue.
- Legacy hash links are preserved: main.js rewrites `/#/whois` → `/?tool=whois`
(and `/#/` → `/`) before the router resolves, so old bookmarks still work.
- Standalone pages drop the homepage's fixed-Nav body padding via a
`body.jn-standalone-page` class toggled on the `/tools/:slug` route.
- MtrTest / GlobalLatencyTest depend on store.allIPs (populated by the
homepage). On a standalone page that's empty, so they now fall back to a
validated free-form IP input when no known IPs exist.
- Shortcuts updated to openTool(slug) / data-adv-slug / isToolOpen; tests and
frontend/AGENTS.md updated to match.
Note: the server-side SPA fallback (so pnpm start / prod serve /tools/*) and
per-route meta land in the next change; dev / preview already have fallback.
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>
Extract the verbatim-identical 16-country default locations array and the
store.allIPs filter from MtrTest and GlobalLatencyTest into the shared
use-globalping-measurement composable as GLOBALPING_DEFAULT_LOCATIONS and
selectableIPs(). Pure data/computed extraction; behavior unchanged.
CensorshipCheck is intentionally left untouched (per-country probe limits).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MtrTest / GlobalLatencyTest / CensorshipCheck all hand-rolled the same
"POST to Globalping → poll GET by id → retry while in-progress" loop,
each with its own setTimeout bookkeeping and no unmount cleanup. If the
user closed the drawer mid-poll, timers kept firing and mutated refs on
unmounted components.
Factor that orchestration into frontend/composables/use-globalping-
measurement.js. The composable wraps POST + GET through fetchWithTimeout
(so a hung upstream no longer pins the request forever) and clears any
pending timer / aborts any in-flight controller on onScopeDispose.
Per-call-site result parsing is kept as an onResults callback so each
tool's field mapping stays explicit. CensorshipCheck's two-message error
split (POST vs empty/poll) is preserved via onError(reason).
Co-authored-by: Claude <noreply@anthropic.com>
refactor/01 phase C, commit 1/3. Replaces the last four uses of Bootstrap
Collapse (which also fixes the content-goes-blank-after-expand bug the
user reported at the end of phase B).
New UI components:
- frontend/components/ui/collapsible/ (Collapsible, CollapsibleTrigger,
CollapsibleContent) — based on reka-ui CollapsibleRoot
- frontend/components/ui/accordion/ (Accordion, AccordionItem,
AccordionTrigger, AccordionContent) — based on reka-ui AccordionRoot,
defaults to type=single + collapsible=true to match the Bootstrap
accordion-with-data-bs-parent semantics
Animations:
- Add CSS keyframes jn-collapsible-down/up and jn-accordion-down/up to
style.css, driven by reka-ui's --reka-collapsible-content-height and
--reka-accordion-content-height CSS variables. 200ms ease-out on
[data-state] transitions.
- Chose custom keyframes over tw-animate-css's built-ins to avoid
cross-contamination with existing Bootstrap .collapse / .collapsing
rules while both frameworks coexist.
Migrated components:
- ip-infos/IPCard.vue — ASN info Collapsible (single toggle per
card). Simplified local state: collapseStates dict → plain isAsnOpen
ref (each IPCard instance tracks its own one-shot panel).
- ip-infos/ASNInfo.vue — removed .collapse class from root (the
panel is now controlled by the parent CollapsibleContent wrapper).
- advanced-tools/Whois.vue — Accordion over providers. type=single,
collapsible, default-value="0" so the first provider is expanded,
matching the previous behavior.
- advanced-tools/MtrTest.vue — Accordion over per-country MTR results,
same pattern as Whois.
- advanced-tools/SecurityChecklist.vue — two collapses:
1) Category intro: wrapped in Collapsible around the descriptive
paragraph + trigger icon.
2) Per-item info row: the table structure (header tr + colspan-4 tr)
doesn't compose cleanly with CollapsibleRoot, so this one uses a
plain checklistInfoOpen[index] ref + v-show instead. No animation,
but correct behavior.
Scan verified: zero `data-bs-toggle="collapse"`, `accordion-button`,
`accordion-item`, `data-bs-target="#collapse*"` remaining.
Bug fix: the "content goes blank after expand" issue observed at the end
of phase B is resolved as a side effect — the content is no longer
managed by Bootstrap's Collapse JS that was conflicting with Tailwind
Preflight rules.
Verified: npm run check — 46 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>