[pull] main from jason5ng32:main#105
Merged
Merged
Conversation
Add Node test coverage for shared frontend/common IP validation helpers and reject malformed IPv6 compression. Co-authored-by: Codex <codex@openai.com>
Keep nodemon and code-inspector-plugin scoped to devDependencies. Co-authored-by: Codex <codex@openai.com>
Update the build and runtime stages to Node 24 and exclude local-only files from the Docker build context. Co-authored-by: Codex <codex@openai.com>
Cover referer checks and early validation paths for config, map, MaxMind, and DNS handlers without making external network calls. Co-authored-by: Codex <codex@openai.com>
Set up Tailwind v4 + shadcn-vue infrastructure and replace the first Bootstrap component (Toast) as a vertical slice that proves the approach. Infrastructure (refactor/01 phase A): - Add tailwindcss + @tailwindcss/vite (devDependencies) and wire the plugin in vite.config.js - Add shadcn-vue runtime deps: reka-ui, class-variance-authority, clsx, tailwind-merge, lucide-vue-next - Add components.json and frontend/lib/utils.js (cn helper) - Define design tokens via shadcn CSS variables (oklch, neutral baseColor) in frontend/style/style.css - Wire dark mode: store.setDarkMode now toggles <html>.dark for the Tailwind dark: variant - Coexist with Bootstrap (no removal yet) Toast → vue-sonner (refactor/01 phase B item 1): - Replace internals of widgets/Toast.vue (Bootstrap Toast → vue-sonner) - Keep store.setAlert() public API unchanged so all 5 call sites stay put - Map alertStyle text-success/warning/danger/info to sonner severities Refactor planning: - Add AGENTS.md so future AI sessions know the project layout, JS-only preference, and where to find refactor progress - Add refactor/ with a 4-task plan, completion discipline, and per-task review checklists Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all Bootstrap Offcanvas usage with a shared shadcn-vue Sheet
implementation coordinated by a single store field. This removes all
imperative Offcanvas.show/hide calls and the global event-based mutex.
New UI components (frontend/components/ui/sheet/):
- Sheet.vue — thin wrapper around reka-ui DialogRoot
- SheetContent.vue — 4-sided variants (top/bottom/left/right),
animated via tw-animate-css
- SheetClose.vue — wraps DialogClose with lucide X icon
Store coordination (frontend/store.js):
- Add openSheet state field: 'preferences' | 'navMenu' | 'achievements'
| 'about' | 'tools' | null
- Add setOpenSheet(name) and toggleSheet(name) actions
- Single field naturally enforces "only one open at a time"
Migrated components (all 5):
- widgets/Preferences.vue — side=left, bound to openSheet='preferences'
- Achievements.vue — side=left, bound to openSheet='achievements'
- Footer.vue (About) — side=right, bound to openSheet='about'
- Nav.vue — desktop renders menu inline (no Sheet);
mobile uses side=bottom Sheet bound to
openSheet='navMenu'. OpenPreferences() now
toggles the store instead of doing a DOM
lookup for #offcanvasPreferences.
- Advanced.vue — side=bottom, bound to openSheet='tools',
fullscreen toggle now flips a ref and the
SheetContent class reacts (h-[80%] vs h-full)
instead of mutating style.height via DOM.
Collateral cleanup:
- router/index.js — remove DOM ops on #offcanvasTools; set
store.openSheet based on route match instead.
- widgets/Patch.vue — delete listenOffcanvas() entirely. The mutex
behavior it enforced via 'show.bs.offcanvas' events is now a natural
consequence of the single openSheet field.
- Nav.vue — delete the desktop CSS hack that forced #offcanvasNavbar
to be always visible on lg+ viewports; desktop path is now a plain
v-if="!isMobile" inline render.
- Add tw-animate-css (^1.4.0) for Tailwind v4-compatible animate-in/out
utilities used by SheetContent slide variants.
Zero remaining references to `import { Offcanvas } from 'bootstrap'`.
Inner layout classes (.offcanvas-header, .offcanvas-body, .offcanvas-title)
are still used as pure Bootstrap CSS and will be rewritten in phase C.
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>
…ooltip
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>
refactor/04 complete. Four families of constants that used to live inline
in store.js are now dedicated modules under frontend/data/ with their
own tests. Consumers still read the same store shape — zero call-site
changes required for the 20+ consumers of userAchievements / ipDBs /
mountingStatus / userPreferences.
New data modules (frontend/data/):
- achievements.js
ACHIEVEMENTS_DEFINITIONS — 21 {name, img} entries
createInitialAchievementsState() — factory, returns fresh keyed map
Bonus fix: IAmHuman.img previously used 'achievements/iamhuman.webp'
(missing leading slash). All 21 entries now uniformly start with
'/achievements/'. No consumer had the legacy path string baked in.
- ip-databases.js
IP_DATABASES — 7 {id, text, url, enabled} entries
createInitialIpDBs()— factory for store.ipDBs
buildDbUrl(db, ip, lang) — pure URL template substitution,
extracted from store.getDbUrl so it can
be tested without instantiating Pinia
- default-preferences.js
DEFAULT_PREFERENCES — Object.freeze'd canonical defaults
createDefaultPreferences() — writable copy factory
- sections.js (new shared source of truth)
SECTION_IDS — ['IPInfo', 'Connectivity', ..., 'AdvancedTools']
DEFAULT_SECTION — 'IPInfo'
createMountingStatus() / createLoadingStatus() — key-set factories
Previously, the same 6 section names were duplicated in three places:
store.mountingStatus keys, store.loadingStatus keys (subset), and a
hardcoded local array inside Patch.vue's checkSectionsAndTrack().
Patch.vue now imports SECTION_IDS from data/.
store.js changes:
- Import factories from data/*, replace 60+ lines of inline literal state
with factory calls
- getDbUrl() delegates URL substitution to buildDbUrl pure function
- loadPreferences() uses createDefaultPreferences()
- currentSection default uses DEFAULT_SECTION
Tests (23 new, brings total 23 → 46, all green):
- tests/achievements.test.js — definitions shape, path format,
factory isolation, runtime shape
- tests/ip-databases.test.js — definitions integrity, factory
isolation, buildDbUrl substitution
/ default lang / missing placeholder
/ null-guard
- tests/default-preferences.test.js — freeze, shape, copy isolation
- tests/sections.test.js — id order, factory isolation
Plan updates:
- refactor/04: status → ✅ done, all checkboxes ticked, audit table
(Phase A "audit results") filled in with line numbers and routing decisions
- refactor/README.md: total status for #4 → ✅
- This also ticks the store / utils sections of refactor/03 indirectly
(the new data/ tests plus existing valid-ip and api-smoke suites).
Verified: npm run check — 46 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dion
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>
…removal)
refactor/01 phase C, commit 2/3. Replaces the remaining three families of
Bootstrap JS widgets that were still driven by data-bs-* attributes,
unblocking the final removal of `import 'bootstrap'` in commit 3/3.
New UI components:
- frontend/components/ui/dropdown-menu/
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel
(based on reka-ui DropdownMenu*)
- frontend/components/ui/tabs/
Tabs, TabsList, TabsTrigger, TabsContent (based on reka-ui Tabs*)
Dropdown migrations (2):
- components/Nav.vue — user menu. The sign-in/sign-out/
achievements/benefits/Firebase-level metadata dropdown, with conditional
sections for signed-in vs signed-out and the owner/premium/etc badges,
all flattened into DropdownMenuItem + DropdownMenuSeparator +
DropdownMenuLabel. @click on items → @select on DropdownMenuItem (reka
uses select semantics to also close the menu).
- components/advanced-tools/DnsResolver.vue — query-type picker
(A / AAAA / CNAME / MX / NS / TXT). Loop renders six
DropdownMenuItem entries.
Tab migration (1 component, 2 tabs):
- components/Achievements.vue — the Get / NotGet achievements split.
Replaced the nav-tabs + tab-content + tab-pane structure with Tabs +
TabsList + two TabsTrigger + two TabsContent.
ScrollSpy removal (not a migration, just deletion):
- App.vue — removed data-bs-spy="scroll", data-bs-target, data-bs-
root-margin, data-bs-smooth-scroll. Patch.vue's checkSectionsAndTrack
is already watching scroll events and writing store.currentSection,
and Nav.vue reads store.currentSection to highlight the active link.
Bootstrap's scrollspy was redundant.
Scan verified: zero data-bs-toggle="dropdown"|"tab" and zero
data-bs-spy="scroll" remaining in the codebase.
With this commit the only Bootstrap JS reference left is the global
`import 'bootstrap'` side-effect in main.js, which becomes dead code.
Commit 3/3 will remove it alongside the Bootstrap CSS import and the
package.json dependencies.
Verified: npm run check — 46 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…shim refactor/01 phase C, commit 3/3 — completes the primary goal of the migration: bootstrap is no longer a dependency and no longer loaded as JS or CSS at runtime. What this commit does: - npm uninstall bootstrap — dependency removed from package.json - Delete `import 'bootstrap'` from main.js (dead code after commits 1/3 and 2/3 removed every data-bs-toggle / data-bs-spy driven widget) - Delete `@import 'bootstrap/dist/css/bootstrap.min.css'` from style.css - Add frontend/style/bootstrap-compat.css — a Tailwind-backed shim that reimplements the ~168 Bootstrap semantic classes actually used in the templates, via @apply + a handful of bespoke rules for things like .btn-close's X icon, .form-check's checkbox/switch SVGs, .spinner-grow keyframes, and the responsive .col-*/.col-md-*/.col-lg-* grid system. Covered by the shim: - Display: .d-flex, .d-none, .d-block, .d-lg-*, .d-md-*, .d-inline* - Flex: .align-items-*, .justify-content-*, .flex-column|row|wrap, .flex-grow-1, .flex-shrink-1 - Typography: .fw-*, .fs-1..7, .text-center/start/end, .text-nowrap, .text-decoration-*, .lh-lg, .font-monospace - Colors: .text-white/dark/light/secondary/success/warning/danger/info /muted + their .dark:* overrides .bg-*, .text-bg-*, .bg-body-tertiary - Sizing: .w-100/50/75, .h-100/50/75, .mw-100, .mh-100 - Borders: .border, .border-top/bottom/start/end, .border-dark/light/ subtle, .rounded, .rounded-pill/circle, .shadow - Layout: .container, .container-xxl, .row, .col, .col-1..12, .col-md-*, .col-lg-*, .col-xl-* - Components: .btn, .btn-primary/secondary/dark/light/outline-*, .btn-close, .btn-group, .btn-check .card, .card-body/title/text/header/footer/img-top .modal-*, .offcanvas-* .navbar, .navbar-nav, .navbar-brand, .navbar-toggler* .nav-link, .nav-item .dropdown-toggle (content is shadcn now) .alert + .alert-success/warning/danger/info/primary/light .badge, .list-group*, .table*, .progress*, .spinner-grow* .form-check, .form-control, .form-select, .form-switch, .input-group, .form-label .placeholder, .placeholder-glow .link-dark/light/success, .link-underline-* Bundle impact (measured on `npm run build`): - Main JS chunk: 463 kB → 355 kB (−108 kB, −23%) - CSS: bootstrap.min.css (~200 kB) no longer shipped; the compat shim is only the classes actually referenced, pulled into the Tailwind bundle Still in the tree (intentional, separate work): - bootstrap-icons package + its CSS import — `.bi bi-*` icon spans are used in hundreds of places. Migrating to lucide-vue-next is a separate effort and is logged as a non-blocking follow-up in the plan. - bootstrap-compat.css itself — optional future cleanup is to rewrite templates to use Tailwind utilities directly and then delete the shim. Not blocking. Plan updates: - refactor/01 stage header reflects that bootstrap is gone; stage E checklist ticked for the items actually completed; two remaining non-blocking follow-ups listed explicitly. - refactor/README.md 01 row now says "main body done" and explains what's left. Verified: npm run check — 46 tests pass, vite build succeeds with no runtime `import 'bootstrap'` anywhere in the bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…compat shim" This reverts commit 254b0812f1fff8faab88a30d286b281dc5a9187a.
…plan Follow-up to revert 3c28014. Now that the bootstrap-compat.css shim is gone, this commit does the narrow thing that is actually safe and valuable: Code change: - frontend/main.js: delete `import 'bootstrap'`. After the collapse/ accordion/dropdown/tabs/scrollspy migrations (commits 521a955 and 29b4019), nothing in the app still relies on Bootstrap JS — no data-bs-toggle attribute survives, no component imports Modal/ Offcanvas/Tooltip/Toast from 'bootstrap'. The bundle shrinks by ~80 KB (bootstrap JS chunk) with zero behavioral change. - bootstrap CSS import (`@import 'bootstrap/dist/css/bootstrap.min.css'`) is RESTORED and kept. It is doing real work for 168+ class names across 28 components; removing it requires rewriting those templates to Tailwind, which is a gradual per-component job documented below. - bootstrap package stays in package.json dependencies because the CSS file is imported from there. Plan realignment (the revert rolled plan docs back to the pre-3/3 state; this commit restores truthful status): - refactor/01 status header rewritten to describe current reality: A + B done, C.1 (remaining JS widget migrations) done, C.2 (template class rewrite) paused with an explicit lesson-learned paragraph about why the shim approach was wrong. - The old single phase C list is split into C.1 (✅ done: the 4 dropdown/collapse/tabs/scrollspy migrations that landed in commits 521a955 and 29b4019 and survived the revert) and C.2 (template class rewrite, ordered by visual complexity low→high so each commit can be visually verified against the previous state). - Phase B checklist item "remove `import 'bootstrap'` from main.js" ticked — that is what this commit actually does. - refactor/README.md overview row for #1 updated to the same. Verified: npm run check — 46 tests pass, vite build succeeds, no `import 'bootstrap'` anywhere in the source tree. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First component in phase C.2 (per-component template rewrite from
Bootstrap class names to Tailwind + shadcn-vue).
InfoMask.vue — a single fixed-position three-state button (success /
warning / secondary) that toggles data masking levels:
Approach (per C.2 convention rule 1, "use shadcn-vue when it fits"):
- Base: shadcn-vue <Button size="icon" /> gives the correct icon-only
button shape (h-9 w-9, rounded, focus ring, disabled state, hover
transition, etc.) for free — same footprint Bootstrap's default .btn
produced.
- Color: the 3 state colors (btn-success/warning/secondary in the old
version) have no matching shadcn Button variant, so they layer on via
:class overriding the default variant's bg-primary. twMerge in cn()
handles the bg/hover conflict correctly. If more components need
success/warning buttons later, we can lift these into buttonVariants.
Other cleanups:
- Removed <style scoped>: `.infomask { position: fixed; bottom: 66px;
right: 20px; z-index: 1050; }` → class="fixed bottom-[66px] z-[1050]"
on the Button, with `right` driven by a reactive :style computed from
window.innerWidth (replaces the previous DOM-mutating
adjustButtonPosition that mutated .style directly via querySelector).
- Added onBeforeUnmount cleanup of the resize listener (was leaking in
the original).
- Removed the `document.querySelector('.infomask')` reference chain and
its implicit dependency on the legacy class name.
- Removed unused isDarkMode / isMobile store computeds.
Verified: npm run check — 46 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During phase C.2 of refactor/01, Bootstrap CSS remains loaded while
shadcn-vue components are migrated one by one. Bootstrap's Preflight
and utility rules (e.g. `button{border-radius:0}`, `.shadow{...!important}`)
therefore override shadcn's intended styling — but only during the
transition. These interferences disappear automatically when
bootstrap.min.css is finally removed at the end of C.2.
The correct C.2 behavior when seeing such interferences:
- Do not add !important, do not add scoped overrides, do not reorder
cascade layers
- Check the cause: is it transient interference from Bootstrap, or a
real mistake in the migration (wrong component, missing class)?
- If transient: note it, move on; it resolves itself on Bootstrap removal
- If real: fix it
- Reference standard during migration is shadcn-vue's native default,
not the current Bootstrap rendering
Context: I was about to write workaround CSS for Button's rounded-md /
shadow being suppressed by Bootstrap on the very first C.2 component.
User stopped me with the above reasoning. Principle now lives in
AGENTS.md next to the existing shadcn-first rule and is backed by a
detailed learning entry (LRN-20260417-002).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Second component in C.2 after InfoMask. Also batches in three "nothing
to do" cases documented in the plan: PWA.vue (just wraps
<pwa-install>), Patch.vue (empty template), svgicons/{Brand,IPError}.vue
(pure SVG, only Tailwind-native classes).
New UI components scaffolded from shadcn-vue (each as a small wrapper
around reka-ui primitives, following the existing pattern in
frontend/components/ui/):
- ui/badge/ — Badge + cva variants (default/secondary/
destructive/outline). Colors for other
semantics are layered via :class.
- ui/toggle-group/ — ToggleGroup, ToggleGroupItem (reka-ui
ToggleGroupRoot/Item). Single-select mutually
exclusive buttons with data-[state=on] styling.
- ui/separator/ — reka-ui Separator, horizontal/vertical, default
bg-border.
Footer.vue rewrites:
- The about/changelog/specialthanks 3-way picker was Bootstrap's
"visually-hidden radio + btn-check + labeled .btn" hack. Replaced
with <ToggleGroup v-model="content" type="single"> + three
<ToggleGroupItem>. state management simplified from three booleans
(showAbout, showChangelog, showSpecialThanks) + a toggleContent
mutator to a single `content` ref + v-if.
- <hr> inside changelog version blocks → <Separator class="my-2" />.
- changelog entry type pill (add / improve / fix) was
`.badge .rounded-pill .bg-success|.bg-info|.bg-danger` with text
baseline off. Replaced with <Badge class="rounded-full bg-green-600
text-white border-transparent ...">.
- All `link-dark / link-light / link-success / link-underline-*` links
rewritten as Tailwind text-* + no-underline/hover:underline. Dark
mode handled via `dark:text-*` variants since store.setDarkMode
already syncs <html>.dark.
- Removed obsolete inner offcanvas-* classes (`.offcanvas-header`,
`.offcanvas-body`) inside the Sheet; Sheet handles its own padding
and scroll container, so these were dead class names. Content is
now wrapped in `<div class="p-4" ref="sheetBody">`.
- `<style scoped>`: removed `#About { z-index: 1051 }` (id gone), and
`.jn-placeholder` usage replaced with a plain `<div class="h-6">`.
Only `.jn-heart-color` retained (custom project pink).
Bug fix picked up in passing:
- Original code `offcanvasBody.scrollTop = 0` sets `.scrollTop` on the
Vue ref object itself, not on the underlying DOM node. Silent no-op.
Replaced with a watcher on `content` that does
`sheetBody.value.scrollTop = 0` via nextTick.
Skipped (not in scope):
- `.bi bi-*` Bootstrap Icons font classes remain throughout; those
will be migrated to lucide-vue-next as phase D, not in C.2.
Verified: npm run check — 46 tests pass, vite build succeeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Additional.vue — curl info dialog + 3 cross-promotion image links. - Dialog internals: removed .modal-content/.modal-header/.modal-body/ .modal-footer and :data-bs-theme; Dialog handles structure, and header uses inline Tailwind (flex items-center justify-between pb-3 border-b border-neutral-200 dark:border-neutral-700). - The dark-mode conditional classes (dark-mode / dark-mode-border / dark-mode-close-button) dropped — shadcn Dialog already respects the global .dark class. - Curl code block layout: .row flex justify-content-center → flex flex-col items-start gap-1. Column layout is actually what the original wanted; old code used .row which would have been wrong, but Bootstrap's custom .jn-comment margin-left made it look OK. - Outer promo grid: .container → mx-auto max-w-[98%], d-flex justify-content-center → flex justify-center. - text-success/secondary/light/warning color-coding in code samples → Tailwind text-green-600 / text-neutral-500 / text-neutral-100 / text-yellow-400 (inside dark code block). - Kept .jn-curl / .jn-comment scoped custom classes (they use pseudo-elements, not Bootstrap). Verified: npm run check — build + 46 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Help.vue — keyboard shortcuts modal with a two-column key/description list. - Dialog internals: removed modal-content/header/body wrappers and dark-mode-* conditionals; header is inline flex with Tailwind border. - Two-column grid: Bootstrap's .row flex-nowrap + two .col + inner .row p-2 justify-content-between pattern → flex + flex-1 on each column, and each row is a plain flex with flex-1 description and shrink-0 kbd. Border-bottom for row separator via border-b border-neutral-200 dark:border-neutral-700 (replaces border-dark-subtle/border-light-subtle + .jn-dark-mode-help-border). - <kbd>: was relying on Bootstrap's default kbd styling + optional .text-bg-light class. Now explicit Tailwind: px-1.5 py-0.5 text-xs rounded border + bg-neutral-100 / dark:bg-neutral-700, etc. Verified: npm run check — build + 46 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
User.vue — User Benefits Dialog with a 4-row benefits table. - Dialog internals: modal-content/header/body/footer/.dark-mode-* removed, structure inlined with Tailwind. - Table: .table / .table-dark / .table-responsive → plain <table> with Tailwind (w-full border-collapse + border-b on each tr + p-2 on cells). Dark-mode via dark: variants. - Removed unused isDarkMode / isMobile computeds. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Refresh button: .btn btn-dark/light + dark-mode-refresh → shadcn <Button size=icon variant=outline>. - Grid: .row + .col-6 col-md-3 → flex flex-wrap -mx-2 + w-1/2 md:w-1/4 px-2. - Card: .card + dark-mode-border → plain rounded-lg border bg-card. - Text colors: text-info/success/danger → text-sky/green/red-600. .jn-text-warning (custom #c67c14) kept in scoped style. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as ConnectivityTest + inner .alert block. - Refresh button → <Button size=icon variant=outline>. - Grid: .col-lg-3 col-md-6 col-12 → w-full md:w-1/2 lg:w-1/4 px-2. - Card → Tailwind rounded-lg border bg-card. - .alert alert-info/alert-success → conditional Tailwind color block (bg-sky-50/green-50 + border-* + text-* + dark: variants). - .fw-bold → font-bold. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as ConnectivityTest / DnsLeaksTest. - <Button size=icon variant=outline> for refresh. - Grid: flex flex-wrap -mx-2 + w-full md:w-1/2 lg:w-1/4. - .card → Tailwind. - .alert alert-info/success → conditional Tailwind color blocks. - Text color mapping: text-info/success/danger → text-sky/green/red-600. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers Empty.vue (no work) and BrowserInfo.vue migration. New UI component: - ui/switch/ — shadcn-vue Switch wrapping reka-ui SwitchRoot/Thumb. BrowserInfo.vue: - .row/col-lg-8/col-md-8/col-12 → flex/flex-wrap + w-2/3 / w-1/3. - .card → rounded-lg border bg-card. - .alert alert-success/primary/light → Tailwind colored blocks with dark: variants. - .badge text-bg-success/primary → shadcn <Badge> + bg-*. - .form-check form-switch → shadcn <Switch v-model>. - .spinner-grow text-success → inline animate-pulse dot. - Removed <style scoped> rules for .jn-ua-box (replaced with Tailwind inline), kept .jn-code-font / .jn-detail / .jn-fp-box-mobile / .jn-placeholder / slide-fade-* (project custom, not Bootstrap). - Removed unused isDarkMode computed. Verified: npm run check — build + 46 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Search input-group → flex + <Input> + <Button> (rounded-r-none / rounded-l-none -ml-px for joined look). - Card + table: .table / .table-hover / .table-dark → Tailwind (w-full, border-collapse, hover:bg-neutral-50, dark: variants). - .col-lg-8 / col-md-4 / col-12 grid → flex flex-wrap -mx-2 + w-2/3 / w-1/3. - .text-success / .text-secondary icon colors → text-green-600 / text-neutral-500. - .spinner-grow → inline animate-pulse dot. - Dropped unused isMobile / isDarkMode computeds. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Card grid pattern: row/col-lg-3/col-md-6/col-12 → flex flex-wrap -mx-2 + w-full md:w-1/2 lg:w-1/4. - .alert alert-info/danger/success → 3-way conditional Tailwind color block w/ dark: variants. - Refresh button → <Button> + state-driven bg-green-600/bg-sky-600. - Spinner → animate-pulse dot. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Input group (Input + Type Dropdown + Run Button) uses rounded-* to visually join: Input rounded-r-none, Dropdown trigger rounded-none, Run button rounded-l-none; -ml-px for joins. - .btn.btn-primary → <Button> with bg-blue-600. - .table / .table-hover / .table-dark → Tailwind table with hover:bg-* and dark: variants. - Spinner → animate-pulse dot. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Input + Button layout (Input rounded-r-none + Button rounded-l-none). - .alert alert-success success banner → Tailwind color block. - .card card-body bg-light/bg-black raw pre container → Tailwind rounded + bg-neutral-100/bg-black. - Spinner → animate-pulse dot. Verified: npm run check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI was failing on every push with: Error: Dependencies lock file is not found... Supported file patterns: package-lock.json, npm-shrinkwrap.json, yarn.lock `actions/setup-node@v4`'s `cache: npm` uses a lockfile hash as its cache key — without one it errors out rather than degrading gracefully. The repo intentionally gitignores package-lock.json (see .gitignore), so the cache line was broken from the moment the workflow was added. Simplest fix: drop `cache: npm`. `npm install` on this project is ~30s cold — acceptable cost versus committing a lockfile we don't otherwise use. Replace the misleading comment that claimed "npm cache uses package.json" (setup-node does not actually support that). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The language switcher at the top of every README has listed Türkçe since the tr locale was added, but the "also supports" bullet lower in the page was never updated to match — all four READMEs still read "English, Chinese, and French". Catches the four translations up. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
GitHub Actions is deprecating Node 20 as the runtime for JavaScript actions (checkout, setup-node, etc.). Every workflow in this repo currently emits: Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4, actions/setup-node@v4. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Set `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true` at the job level on all three workflows so they switch to the Node 24 runner now. After 2026-06-02 this is the default and the env var can be removed. Note: the `node-version: 24` in ci.yml already pins the Node runtime for the *project's* tests / build — this is unrelated; it controls the Node version the *actions themselves* run on, which is a separate concern. Also bumped `actions/checkout@v3` → `@v4` in sync.yml while here — v3 stops receiving Node-runtime updates and v4 is already the standard in the other two workflows. See: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Both actions published v6 in late 2024/early 2025 with `runs.using: node24` declared natively, so they no longer emit the Node 20 deprecation warning nor need the `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` env-var opt-in that the previous commit added. Upgrading directly is cleaner than keeping the workaround and lets the workflow files shed the accompanying comment blocks. No breaking changes affect this project's usage (checkout + setup-node with `node-version` only). The third-party `aormsby/Fork-Sync-With-Upstream-action@v3.4` in sync.yml has shipped on node24 since v3.4.2, so no env-var fallback is needed there either. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
CodeQL flagged: Workflow does not contain permissions Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Both ci.yml and docker-image.yml relied on the implicit default token permissions (broad read+write across the repo). Explicit scoping: - ci.yml: `contents: read` — the job only runs tests + build, no mutations. - docker-image.yml: `contents: read` + `packages: write` — checkout plus the GHCR push via GITHUB_TOKEN. Docker Hub push uses its own repo secret (DOCKER_HUB_ACCESS_TOKEN), unaffected. sync.yml already had `permissions: contents: write` at the top level, so no change there. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…rs from user choice
Two overlapping bugs:
1. In Preferences.vue, the old handleThemeChange read
`userPreferences.theme` and then called `store.setDarkMode` + class
toggles, but `prefTheme('auto')` invoked it *before* persisting the
newly-selected theme value, so switching to Auto could stick on the
previous manual state. The refactor funnels every trigger (mount / OS
flip / preference change) through a single `applyTheme()` that reads
the current values and applies them once. A `watch` on the theme pref
keeps the handler firing even if future code paths mutate it directly,
and `onUnmounted` removes the matchMedia listener for cleanliness.
2. In index.html, the inline `<style>` had a
`@media (prefers-color-scheme: dark) { html { background: #0a0a0a } }`
override. That rule is gated strictly on the OS preference and ignores
the `.dark` class that Tailwind (and Preferences.vue) use to control
dark mode. With system=dark + user=light, Tailwind correctly flipped
to light but the inline media query kept the `<html>` background
painted dark — which then showed through every gap in the light-mode
layout under the Sheet overlay, producing the UI in the report.
Swap the media query for `html.dark { ... }` so the inline CSS now
respects the same class-based switch. Seed that class before first
paint via a short inline script at the top of `<head>` — reads
`localStorage.userPreferences.theme`, falls back to
`prefers-color-scheme` on auto, adds `.dark` if needed. This keeps the
boot screen and the mounted app in agreement from frame 1 (no FOUC,
no flash).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The GeoLite2 `.mmdb` files are no longer tracked in the repo (MaxMind's EULA prohibits redistribution), so fresh deployments — especially via Docker — start with an empty `common/maxmind-db/` directory and serve 503s from `/api/maxmind` until credentials are configured. The README still labeled the MaxMind env vars as optional, which misled users. Changes (applied to en / zh / fr / tr READMEs): - `MAXMIND_ACCOUNT_ID`, `MAXMIND_LICENSE_KEY`, `MAXMIND_AUTO_UPDATE` flip to **Yes** in the Required column, with notes clarifying that `AUTO_UPDATE` can stay false only if `.mmdb` files were pre-seeded manually. - Reordered the table so all required vars sit at the top. - New "MaxMind Databases (Required)" subsection before the table, explaining why the vars matter, giving the exact signup + license-key steps, and including a prominent warning for Docker deployers that a missing setup silently breaks the MaxMind IP source and the WebRTC country badges. - Updated the Node and Docker env-var examples to include the three MaxMind variables so copy/paste deployers end up with a working configuration by default. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The docker-image workflow was emitting: Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: docker/build-push-action@v5, docker/login-action@v3, docker/setup-buildx-action@v3. All three have newer majors that declare `runs.using: node24` natively: docker/setup-buildx-action v3 → v4 docker/login-action v3 → v4 docker/build-push-action v5 → v7 Usage is unchanged (push / tags / labels / platforms are stable across the bump). The one visible side effect is that build-push-action v6+ enables attestations (provenance + SBOM) by default — both Docker Hub and GHCR accept them, so the publish still succeeds; the registry UI will just show an additional attestation manifest under each tag from this release onward. Set `provenance: false` if that's unwanted later. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update Readme
Docker Hub's repo overview page needs to be manually kept in sync with README.md — easy to forget, and often drifts out of date. Add a small dedicated workflow that pushes README.md to Docker Hub whenever it changes on main. Trigger is scoped with a `paths` filter to README.md + this workflow file itself, so unrelated pushes don't waste a run. `workflow_dispatch` remains available for the first-time activation and for forcing a resync after a token rotation. Uses peter-evans/dockerhub-description@v5 (Node 24 native, released in October 2025). The reused DOCKER_HUB_ACCESS_TOKEN secret works if it was created with "Read, Write, Delete" scope — the Docker Hub metadata API requires wider permissions than the registry push path that `docker/login-action` goes through. A comment in the workflow flags this so whoever hits a 403 later knows what to rotate. Kept separate from docker-image.yml because README changes are logically unrelated to image releases — bundling them would either spam rebuilds or miss docs-only updates. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Feat(ci): auto-sync README.md to Docker Hub on every push
…n read secrets The previous commit wired the README-sync workflow to DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN, but those secrets live on the `production` environment (same as docker-image.yml), not at the repository level. Without `environment: production` on the job, those references resolve to empty strings and peter-evans/dockerhub-description aborts with: Error: Required input 'username' is missing. Add the environment declaration to match docker-image.yml's pattern. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )