Commit Graph
46 Commits
Author SHA1 Message Date
jason5ng32andClaude Opus 5.5 c29e48687b Refactor(layout): split backend-only code out of common/, group frontend utils
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>
2026-10-05 13:10:02 +08:00
jason5ng32andClaude Fable 5.1 901092ef66 Feat(asn): ASN Profile tool
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>
2026-10-03 21:33:51 +08:00
jason5ng32andClaude Fable 5.1 c55634c8e0 Feat(asn): frontend groundwork for the ASN Profile tool
- 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>
2026-10-03 19:47:15 +08:00
jason5ng32andClaude Fable 5 4af258a069 Feat(ipinfo): expose map and traffic panels inline in QueryIP
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>
2026-08-26 18:18:57 +08:00
jason5ng32 8f2751b15f Fix(logging): log 403 IP-source fail-over as warning
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.
2026-08-23 12:00:10 +08:00
jason5ng32andClaude Fable 5 30da86042f Refactor(i18n): central locale registry with glob discovery and exact-first matching
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>
2026-08-22 19:50:35 +08:00
jason5ng32 26ab6ace3b Improvements 2026-08-15 20:50:54 +08:00
jason5ng32 73e2ca0e46 Improvements 2026-08-14 11:59:36 +08:00
jason5ng32 2fc511cf53 Improvements 2026-08-12 23:19:38 +08:00
jason5ng32andClaude Opus 5 f0a424eed0 Feat(ip-forms): reject unreachable targets before any request goes out
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>
2026-08-11 21:30:04 +08:00
jason5ng32 f2d0405a9a Improvements 2026-06-08 15:09:48 +08:00
jason5ng32andClaude Opus 4.8 19ae2427a6 Refactor(ui): consolidate floating buttons into FloatingDock container
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>
2026-05-30 11:54:18 +08:00
jason5ng32andClaude Opus 4.8 3eac8b7443 Refactor(frontend): rename use-analytics to analytics
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>
2026-05-30 11:04:17 +08:00
jason5ng32 f728693769 UI Improvements 2026-05-27 11:10:54 +08:00
jason5ng32 8fdd208392 Improvements 2026-05-27 10:56:27 +08:00
jason5ng32 5fcb1dd2fe UX Improvements 2026-05-27 02:46:56 +08:00
jason5ng32andClaude Opus 4.7 bb0465a220 Feat(ip-info): ASN upstream connectivity graph from CAIDA data
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>
2026-05-13 16:17:21 +08:00
jason5ng32andClaude Opus 4.7 7e75ebf89c Fix(query-ip): proper source fallback + NoData error surface
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>
2026-05-13 09:16:52 +08:00
jason5ng32andClaude Opus 4.7 4b8b16d212 Feat(ip-infos): add ASN routing history per IP card
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>
2026-05-12 21:07:28 +08:00
jason5ng32andClaude Opus 4.7 320f2303fe Refactor(ip-display): fit-to-width IP text across all IP rows
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>
2026-04-20 12:08:12 +08:00
jason5ng32andClaude Opus 4.7 e78fa8b77b Fix(forms): harden free-form inputs against iOS keyboard quirks
- 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>
2026-04-20 10:17:25 +08:00
jason5ng32andClaude Sonnet 4.5 c01e984159 Fix(a11y): silence Dialog description warning + extend aria-invalid to 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>
2026-04-19 18:18:35 +08:00
jason5ng32andClaude Sonnet 4.5 df91967f1b Style(toast): move toasts to bottom-right, left of the FAB column
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>
2026-04-19 17:14:56 +08:00
jason5ng32 5efab6172c Improvements of UI 2026-04-19 09:58:34 +08:00
jason5ng32 f8c5851686 Improve comments 2026-04-19 08:24:49 +08:00
jason5ng32andClaude Opus 4.7 8d3fad9ce2 feat(ip-infos): on-demand Map Dialog + 2-up card grid + shared IpDetailPanel
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>
2026-04-18 11:54:32 +08:00
jason5ng32andClaude Opus 4.7 9fad04f72c feat(ui): new Spinner primitive + QueryIP querying state
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>
2026-04-17 19:17:35 +08:00
jason5ng32andClaude Opus 4.7 40bced9cfb feat(ui): add --action semantic color token + Button action variant
Introduce a semantic token for "run action / trigger action", so we stop
writing hard-coded bg-blue-600:
- light: blue-500 (oklch(0.623 0.214 259.815)) + white foreground
- dark: blue-400 (oklch(0.707 0.165 254.624)) + dark foreground
  (one-tier brighter in dark mode, matching info / success / warning rhythm)

Button variant adds action: bg-action text-action-foreground shadow hover:bg-action/90

QueryIP applies:
- Floating search button (bottom-right) → variant="action"
- Dialog submit button → variant="action"
- Inside InputGroup, the button's rounded / border / shadow are flattened
  by the group, but bg-action / hover:bg-action/90 pass through; hover
  shows a subtly darkened same-blue, transition feels natural

Follow-up: the 5 "bg-blue-600 hover:bg-blue-700" hard-codes inside
advanced-tools can then move to variant="action" to track the theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:14:22 +08:00
jason5ng32andClaude Opus 4.7 590a4aa5e3 refactor(ui): new InputGroup primitive + QueryIP switches to it
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>
2026-04-17 19:07:09 +08:00
jason5ng32andClaude Opus 4.7 7b2b3c6738 refactor(queryip): fully align visuals with IPCard + switch to circle-flags
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>
2026-04-17 18:55:42 +08:00
jason5ng32 22a4392c96 fix(speedtest): reserve Progress / Colo space to avoid page shift when test starts
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.
2026-04-17 16:37:41 +08:00
jason5ng32 fb2aab2510 refactor: bootstrap-icons → lucide-vue-next — phase 01 D DONE
* 70 bi-* icons mapped to lucide components one by one
* 21 .vue files, ~101 <i class="bi bi-*"> call sites all replaced
* Visual parity: uniformly attach class="inline size-[1em] align-[-0.125em]"
  to reproduce bootstrap-icons' font-size-following sizing + baseline offset
* Dynamic-class scenarios switched to <component :is="...">
* SecurityChecklist / ConnectivityTest i18n data still uses bi-* strings as
  keys; components add a map that translates string → lucide component (to
  avoid editing i18n data)
* Whois / RuleTest / DnsLeaksTest / IPCard numbered-circle icons replaced
  with plain Tailwind number badges (lucide has no numbered icon)
* Removed the bootstrap-icons import from style.css
* npm uninstall bootstrap-icons — package.json no longer contains bootstrap*
* refactor/01 fully wrapped (A+B+C.1+C.2+D+E all ✅)
2026-04-17 12:40:54 +08:00
jason5ng32andClaude Opus 4.6 daf49b8ea8 refactor: rewrite QueryIP template to Tailwind + shadcn (5/N for C.2)
- 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>
2026-04-17 11:25:23 +08:00
jason5ng32andClaude Opus 4.6 4cb1769ddb refactor: tooltip rewrite — drop custom directive, adopt shadcn-vue Tooltip
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>
2026-04-15 12:13:20 +08:00
jason5ng32andClaude Opus 4.6 f3efd54d2c refactor: tooltip directive rewrite + 4 modals → shadcn-vue Dialog
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>
2026-04-15 11:57:32 +08:00
jason5ng32 821ff08a1c Improvements 2025-07-02 01:02:19 +08:00
jason5ng32 eda8d9e872 Improvements 2025-07-01 20:50:11 +08:00
jason5ng32 32ecb3dbb5 Improvements 2025-07-01 20:11:37 +08:00
jason5ng32 072241a7dc Improvements 2025-01-27 01:26:33 +08:00
jason5ng32 87182c8f09 Improve User system 2025-01-26 12:33:00 +08:00
jason5ng32 dcf94be7ee Add IP Score 2024-12-09 15:54:26 +08:00
jason5ng32 2f7dfdff04 UI bug fixed 2024-05-28 09:32:48 +08:00
jason5ng32 6ec6d8498a Remove unnecessary codes 2024-05-15 13:45:55 +08:00
jason5ng32 3ca81d09eb 小修复 2024-05-13 17:18:05 +08:00
jason5ng32 3394613e87 完成 ipvue 的重构 2024-05-13 15:08:05 +08:00
jason5ng32 20ca2045b4 多语言和 GA 的局部引入改造完成 2024-05-13 10:13:20 +08:00