Skip to content

[pull] main from jason5ng32:main#105

Merged
pull[bot] merged 183 commits into
Cosr-Backup:mainfrom
jason5ng32:main
Apr 19, 2026
Merged

[pull] main from jason5ng32:main#105
pull[bot] merged 183 commits into
Cosr-Backup:mainfrom
jason5ng32:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 19, 2026

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 : )

jason5ng32 and others added 30 commits April 15, 2026 01:26
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>
jason5ng32 and others added 25 commits April 19, 2026 13:34
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>
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>
@pull pull Bot locked and limited conversation to collaborators Apr 19, 2026
@pull pull Bot added the ⤵️ pull label Apr 19, 2026
@pull pull Bot merged commit 96b7feb into Cosr-Backup:main Apr 19, 2026
3 of 4 checks passed
@4everland 4everland Bot requested a deployment to production April 19, 2026 12:27 Abandoned
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant