Commit Graph

18 Commits

Author SHA1 Message Date
Sanaei f52c3c4837 perf(clients): make the clients page scale to large panels
The clients page was slow on panels with many clients for two independent
reasons: the server rebuilt the whole picture on every request, and the
browser rebuilt the whole table on every poll.

Server side, ListPaged loaded every client row, every client_inbounds link
and every client_traffics row into Go memory, then filtered, sorted and
paginated in a loop -- on a request the page repeats every five seconds.
Every predicate now runs in SQL and only the requested page's ids are
hydrated, so the cost tracks the page size rather than the client count.
Measured on SQLite with a realistic status mix: the default view at 100k
clients goes from 1,072ms to 64ms. Behaviour is preserved deliberately in
the subtle places -- the cross-panel global-traffic overlay is folded into
the same used-bytes expression the predicates and sort use, LIKE wildcards
are escaped so a search for "a_b" stays literal, and the two different
tiebreak rules the in-memory comparator had are reproduced per sort key.

The summary's per-bucket email lists are capped at 200 with exact counters
beside them. They only back hover popovers, but shipping every match made
the response grow with the panel: at 100k clients it carried ~42k emails,
and the page revalidated all of them through a strict Zod parse every five
seconds. The popover now shows a "+N" chip for the remainder.

Browser side, the page fired three sequential list requests per load and
threw the first two away: the query went out before the persisted sort was
applied, and again before the configured page size was known -- 0 meaning
"one long page" is indistinguishable from "not loaded yet". The page size
is now derived rather than mirrored through an effect, and the previous
visit's value is remembered so the single request goes out at mount instead
of queueing behind /setting/defaultSettings.

Then the per-poll work. Reading isFetching made it a tracked property, so
the refetch interval notified twice per cycle and re-rendered the page even
when structural sharing left the data identical. Xray reports a traffic row
per client whether or not it moved bytes, so the speed map was mostly zeros
and was replaced wholesale every push; zero rows are now dropped and an
unchanged result returns the previous object, which lets React bail out
instead of re-rendering. The five Tooltip-wrapped buttons and the inbound
chips per row do not depend on traffic at all and are now memoised, keyed on
the email because a push replaces the row object of every client whose
counters moved. antd's hashed:false drops 3,311 :where(.css-<hash>) wrappers
and 29% of the generated stylesheet, and a pinned cssVar key stops each of
the eleven page-level ConfigProviders minting its own token scope.

Two callers that only need the mutations, GroupsPage and ClientBulkAddModal,
no longer start the list query -- the groups page had been polling the full
paged list every five seconds for data it never renders.
2026-07-30 02:49:32 +02:00
MHSanaei 9f760cf0fa fix(frontend): stop group modals clearing selection on background refetch
The reset effect in GroupAddClientsModal and GroupRemoveClientsModal
depended on the memoized rows, which are rebuilt whenever GroupsPage
re-renders because candidates/members are inline-filtered arrays. The
5s client-list poll re-renders the page, so any selection made in the
modal was wiped a few seconds later. Reset only when the modal opens.
2026-07-02 23:00:04 +02:00
nima1024m 71aca2018a feat(a11y): screen-reader & keyboard accessibility across the panel (#5486) (#5652)
* feat(a11y): label list, toolbar & dashboard actions for screen readers

Phase 1 of #5486 (Android TalkBack support). Icon-only controls across
the management surfaces previously announced only their untranslated
icon name (e.g. "edit", "ellipsis") or nothing at all.

- Add aria-label to icon-only row-action and toolbar buttons across
  inbounds, clients, groups, hosts, nodes and xray
  (outbounds/routing/dns/balancers) lists, plus the dashboard cards.
- Make clickable bare icons and AntD Card actions keyboard-operable via
  role/tabIndex + Enter/Space (new activateOnKey helper); convert mobile
  dropdown triggers to buttons so they open from the keyboard.
- Fix the sidebar hamburger's mislabeled aria-label (was the dashboard
  label) and translate previously-hardcoded outbound menu labels.

New i18n keys in all 13 locales: sort, menu.openMenu,
pages.xray.outbound.moveToTop.

* feat(a11y): label modal, QR and copy/download controls for screen readers

Phase 2 of #5486. Modal and overlay controls relied on tooltips (not a
reliable accessible name) or were bare clickable icons with no keyboard
or screen-reader support.

- Add aria-label to copy/QR/download/info icon buttons in the inbound and
  client info modals, sub-links modal, QR panel, backup/log modals, and
  to the bare search/select inputs of the attach/detach client modals.
- Make click-to-copy QR codes and the IP-log refresh/clear, geofile
  reload and log refresh icons keyboard-operable (role/tabIndex +
  Enter/Space) with translated labels.
- Label the 2FA code input; drop the QrPanel download-image string
  fallback now that the key exists.

New i18n key in all 13 locales: downloadImage.

* feat(a11y): label form fields and shared form components for screen readers

Phase 3 of #5486. Form controls and shared form widgets were largely
unlabelled, and several remove controls were not keyboard-operable.

- SettingListItem now ties its title to the control via aria-labelledby,
  giving accessible names to the ~90 settings-tab inputs at once.
- InputAddon gains button semantics (role/tabIndex/Enter+Space) and an
  ariaLabel prop when used as an interactive remove control.
- Sparkline charts expose a role="img" summary of their latest values.
- Add aria-label to add/remove/regenerate icon buttons and bare
  inputs/selects across inbound, client and xray (dns/routing/balancer/
  outbound) forms; make clickable remove icons keyboard-operable; mark
  decorative help/target icons aria-hidden; label the JSON editor,
  date-time clear button, header-map remove, notification select-all and
  remark token chips.

New i18n keys in all 13 locales: regenerate, jsonEditor,
pages.xray.balancer.{costMatch,costValue,costRegexp}.

* chore(a11y): add eslint-plugin-jsx-a11y harness and fix flagged interactions

Phase 4 of #5486. Adds eslint-plugin-jsx-a11y (recommended ruleset,
scoped to .tsx) so screen-reader/keyboard regressions fail lint.

- Make the mobile node-card header a proper keyboard disclosure
  (role=button, aria-expanded, Enter/Space activation that ignores
  clicks on the nested action buttons) and drop the now-redundant
  stop-propagation click handlers the linter flagged on card-action
  wrappers in the node, client and inbound mobile cards.
- Disable jsx-a11y/no-autofocus: the autofocus on the login field and
  modal primary inputs is intentional focus management that helps
  screen-reader and keyboard users land on the right control.

make lint passes with the a11y ruleset enforced.

* feat(a11y): cover remaining deferred spots (settings tabs, sockopt, API docs)

Completes the panel sweep for #5486 by labelling the spots previously
left out of phases 1-4:

- NotifyTimeField (Telegram notifications): the mode, interval, unit and
  custom-cron inputs now carry aria-labels.
- The Sockopt toggle in transport options.
- Settings category tabs in icons-only (mobile) mode now expose the tab
  name as the icon's aria-label instead of the raw icon name.
- The Swagger API-docs view is wrapped in a labelled region landmark.

New i18n keys in all 13 locales: pages.settings.notifyTime.{interval,unit}.

* feat(a11y): label shared xray form components and remark field

Code review surfaced frontend/src/lib/xray/forms/ — shared form components
used by the host and inbound JSON forms — which the initial audit missed.

- FinalMaskForm (TCP/UDP final-mask editor): label the icon-only add and
  regenerate buttons and make all six remove icons keyboard-operable
  (role/tabIndex/Enter+Space); adds useTranslation to its sub-components.
- CustomSockoptList: the remove icon is now keyboard-operable.
- SniffingFields: aria-label on the otherwise label-less destOverride select.
- RemarkTemplateField: aria-label on the remark-variable picker button.

New i18n key in all 13 locales: pages.inbounds.sniffingDestOverride.

* feat(a11y): label client info modal and WireGuard config block

After rebasing onto the WireGuard client-config feature, re-apply the
ClientInfoModal copy/QR/IP-log aria-labels (the modal was restructured
upstream, so the original labels did not carry over) and label the new
ConfigBlock component's copy/download/QR actions. ConfigBlock's action
wrapper keeps its stop-propagation handler (a non-interactive guard for
the Collapse header) under a scoped jsx-a11y exception.

* fix(frontend): let npm install jsx-a11y under ESLint 10

eslint-plugin-jsx-a11y@6.10.2 declares a peer range that stops at ESLint 9,
but the panel is on ESLint 10, so `npm ci` aborts with ERESOLVE even though
the plugin runs fine on ESLint 10 with flat config. Add an npm override so
jsx-a11y accepts the project's ESLint version. This keeps normal peer
resolution (recharts' react-is peer still auto-installs) — no global
legacy-peer-deps and no manual react-is pin needed.

* fix(a11y): size mobile row triggers and move node expand role to chevron

Address automated review on #5652:
- add size="small" to the inbound/client/node mobile-card "more" dropdown
  triggers so they match the adjacent small Switch and the established
  desktop RowActions pattern.
- move the node card-head disclosure semantics (role/tabIndex/aria-expanded/
  keyboard) onto the chevron affordance so the expand control is no longer a
  role="button" wrapping the Switch, info button and dropdown. Mouse
  click-anywhere-to-expand is preserved on the header div.
2026-06-29 12:51:29 +02:00
MHSanaei 9b8a0c9b17 feat(groups): reset group traffic without touching client counters
The group page shows traffic counting per group, but the only reset
available zeroed every member client's up/down counters (and their
quotas) via bulkResetTraffic. Group traffic is a derived sum of client
traffic, so zeroing the group display previously required mutating the
clients themselves.

Add a display-only baseline: ClientGroup gains reset_up/reset_down
columns (additive, handled by AutoMigrate). ResetGroupTraffic snapshots
the group's current up/down sum into the baseline, and ListGroups now
reports max(0, sum - baseline). Client counters are left untouched and
no Xray restart is triggered. A new POST /panel/api/clients/groups/
resetTraffic endpoint drives it, creating the client_groups row when the
group exists only as a derived label.

The groups page action now calls the new endpoint; confirm/success
strings updated across all 13 locales to reflect group-only semantics.
2026-06-27 16:33:36 +02:00
MHSanaei 6964d84742 feat(reality): add live REALITY target scanner with IP/CIDR discovery
Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate.

- Single-domain validate auto-fills target and serverNames from the cert SAN
- Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe
- New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries
- Add scanner strings to all 13 locales
- Replace deprecated AntD Alert message prop with title across the panel
2026-06-26 22:18:47 +02:00
tonymoses10 b5872af279 Frontend operation button size optimization (#5343)
* Update ClientsPage.tsx

* Update GroupsPage.tsx

* Update RowActions.tsx

* Update NodeList.tsx

* Update BalancersTab.tsx
2026-06-15 14:26:51 +02:00
MHSanaei 6e20588236 style(ui): enlarge row action icons and rebalance clients table widths
Bump row action icons to 18px across the clients, inbounds, groups and
nodes tables for better visibility.

In the clients table, cap the Client column at 220px and give Duration a
fixed width so the Traffic column becomes the flexible one that absorbs
the remaining horizontal space instead of Client growing oversized.
2026-06-12 18:25:27 +02:00
MHSanaei 85983eec1a refactor(groups): restyle traffic summary into upload/download + usage cards
Split the group traffic summary into two inbound-style cards: a "Total
upload / download" card with up/down arrow icons and a "Total Usage" card
with the pie icon. Add the totalUpDown label across all locales.
2026-06-12 16:22:30 +02:00
MHSanaei 1c5cb84492 feat(groups): show upload/download breakdown in group traffic
Add per-group up/down to GroupSummary (backend + schema), surface them
as Upload/Download columns in the groups table, and fold upload/download
into the Total traffic summary card. Rename the group "Clients in group"
column to just "Clients" across all locales.
2026-06-12 15:30:41 +02:00
MHSanaei 3092326d9e refactor: replace custom geo manager with Xray-core native geodata auto-update
Remove the panel-side custom geo download feature (service, controller,
/panel/api/custom-geo/* endpoints, CustomGeoResource model, UI tab) in
favor of Xray-core's native geodata section
(https://xtls.github.io/config/geodata.html).

- pass the top-level "geodata" key through xray.Config so it survives
  the template round-trip into the generated config
- add a Geodata Auto-Update section to the Xray Updates modal that
  edits geodata (cron schedule, download outbound, asset list) in the
  config template and restarts Xray on save
- previously downloaded geo files in the bin folder keep working in
  ext: routing rules; the orphaned custom_geo_resources table is left
  in place so existing source URLs stay recoverable
2026-06-10 18:27:12 +02:00
Sanaei 1fa51cf0f2 feat(groups): show used traffic per group in groups table
Sum up+down across each group's clients via a LEFT JOIN on email in
ListGroups, expose it as trafficUsed on GroupSummary, and render it as a
new column plus a "Total traffic" summary card. Drops the unused "Empty
groups" card and its translation key.
2026-06-08 23:47:59 +02:00
MHSanaei b9cbc0c1e8 fix(ui): exit infinite spinner with a retry card on failed initial load
List pages wrapped content in <Spin spinning={!fetched}> where 'fetched' only flipped true once data arrived. With staleTime: Infinity + retry: 1, a transient network error on first load left the query in a permanent error state and the spinner stuck forever.

Now 'fetched' also settles on query.isError, and a failed load shows a Result error card with a Refresh button that self-heals when the backend returns, mirroring the existing XrayPage pattern. Applied to clients, inbounds, groups, nodes, and the dashboard.

Fixes #4723
2026-06-01 07:43:32 +02:00
Sanaei d1882c7f29 refactor(frontend): reorganize source tree & break down oversized modals/tabs (#4698)
* refactor(frontend): reorganize components & pages into feature folders

No behavior change; pure file relocation + import path updates.

* refactor(frontend): move shared protocol enums to schemas/protocols/shared

Decouple Outbound from Inbound schemas: SSMethodSchema and VmessSecuritySchema (shared between inbound & outbound) now live in a neutral schemas/protocols/shared/ module. Outbound no longer reaches into schemas/protocols/inbound/*. Pure relocation + import rewiring; schema values identical, snapshots & golden tests unchanged.

* refactor(frontend): break InboundList into helpers/types/RowActions/columns hook/stats modal

InboundList.tsx 781 -> 203 lines. Extracted pure helpers (network labels, sort fns, isInboundMultiUser), shared types, the row-actions menu/cell, the table columns hook, and the mobile stats modal into the list/ folder. Code moved verbatim; no behavior change. typecheck/lint/test/build green, 337 tests pass.

* refactor(frontend): extract InboundInfoModal helpers, types & buildInboundInfo

InboundInfoModal.tsx 1081 -> 836 lines. Moved the pure data helpers (network host/path readers, link-protocol check, copy/download/statsColor/IP formatting) plus all shared types and the buildInboundInfo data builder into info/helpers.ts and info/types.ts. The state-coupled render body is left intact (no React render tests to guard a deeper split). Code moved verbatim; no behavior change. All gates green, 337 tests pass.

* test(frontend): add React Testing Library + jsdom render-test harness

- vitest projects: node unit tests stay lean; new jsdom 'components' project runs *.test.tsx
- component setup: matchMedia/ResizeObserver/localStorage polyfills, react-i18next init, persian-calendar-suite stub (only used under jalali locale)
- smoke + field-label structure snapshots for Inbound & Outbound form modals
- establishes the regression net required before decomposing the oversized form modals
- 341 tests pass (337 unit + 4 component); typecheck/lint/build green

* test(frontend): per-protocol field-structure coverage for both form modals

- drive the protocol Select in jsdom and snapshot rendered Form.Item labels for every protocol
- 10 outbound + 10 inbound protocol states captured as the regression net for protocol-core extraction
- add robust select-driving helpers (test-utils) + post-test body cleanup (setup.components)
- 341 tests pass; typecheck/lint green

* refactor(frontend): extract OutboundFormModal constants & stream helpers

OutboundFormModal.tsx 2238 -> 2080. Moved the pure option arrays/sets and the stream-slice helpers (newStreamSlice, hysteriaStreamSlice, isMuxAllowed, buildAddModeValues) into outbound-form-constants.ts and outbound-form-helpers.ts. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.

* refactor(frontend): extract InboundFormModal advanced JSON editors

InboundFormModal.tsx 3129 -> 2863. Moved AdvancedSliceEditor and AdvancedAllEditor (the in-modal JSON slice/all editors) into advanced-editors.tsx along with their adapter-helper imports. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal loopback/blackhole/dns field blocks

Moved the outbound-only protocol field blocks (loopback, blackhole, dns) out of the modal render body into outbound-only-fields.tsx. First render-body extraction behind the per-protocol snapshot net: loopback/blackhole/dns snapshots unchanged -> verified no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal freedom field block

OutboundFormModal.tsx 2063 -> 1753. Moved the freedom protocol field block (domainStrategy, fragment, noises, finalRules) into outbound-freedom-fields.tsx. Verbatim relocation; freedom per-protocol snapshot unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal wireguard field block

OutboundFormModal.tsx 1753 -> 1622. Moved the wireguard protocol field block (address, keypair gen, domainStrategy, peers + allowedIPs) into outbound-wireguard-fields.tsx; dropped now-unused icon/InputAddon/WireguardDomainStrategy imports. Verbatim relocation; wireguard snapshot unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal core protocol fields

OutboundFormModal.tsx 1622 -> 1538. Moved the shared protocol core field blocks (vmess/vless ID, vmess security, vless encryption/reverseTag, trojan/ss password, ss method/uot, socks/http user/pass) into outbound-core-fields.tsx; dropped now-unused schema/option imports. Per-protocol snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): fold OutboundFormModal server address/port block into core fields

OutboundFormModal.tsx 1538 -> 1516. Moved the shared connect-target (address/port) block into OutboundCoreProtocolFields at the same render position; dropped the now-unused SERVER_PROTOCOLS import. Snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split outbound-only protocol forms into per-protocol files

Replace the grouped outbound-only-fields.tsx + outbound-freedom-fields.tsx with one file per protocol under outbounds/protocols/: freedom.tsx, blackhole.tsx, dns.tsx, loopback.tsx (+ barrel). Matches the prompt's 1-file-per-protocol structure. Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split outbound protocol forms into per-protocol files

Replace the grouped outbound-core-fields / outbound-wireguard-fields with one file per protocol under outbounds/protocols/: vmess, vless, trojan, shadowsocks, http, socks, wireguard, freedom, blackhole, dns, loopback (+ shared server-target). Matches the prompt's 1-file-per-protocol structure (per-modal). Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split outbound transport forms into per-transport files

Extract the tcp(raw)/kcp/ws/grpc/httpupgrade transport blocks into outbounds/transport/ per-file components (RawForm, KcpForm, WsForm, GrpcForm, HttpUpgradeForm). xhttp + hysteria transport remain inline for a follow-up. Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal xhttp transport form

Move the xhttp transport block into transport/xhttp.tsx (takes form + onXmuxToggle prop); drop now-unused HeaderMapEditor and MODE_OPTIONS imports from the modal. OutboundFormModal.tsx down to ~1001 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): extract OutboundFormModal tls/reality security forms

Move the TLS and Reality field blocks into outbounds/security/{tls,reality}.tsx; the none/TLS/Reality Radio.Group selector stays in the modal. Drop now-unused ALPN_OPTIONS/UTLS_OPTIONS imports. OutboundFormModal.tsx down to ~918 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split inbound-only protocol forms (tun, tunnel) into per-file

Extract the tun and tunnel protocol blocks from InboundFormModal into inbounds/form/protocols/{tun,tunnel}.tsx (presentational, declarative). First inbound-side per-protocol split. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split inbound wireguard & shadowsocks protocol forms

Extract the wireguard and shadowsocks protocol blocks from InboundFormModal into inbounds/form/protocols/{wireguard,shadowsocks}.tsx (presentational; form + regen handlers / isSSWith2022 passed as props). Drop now-unused Divider + SSMethodSchema imports. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): split inbound vless/http/mixed/hysteria protocol forms

Extract the remaining inbound protocol blocks into inbounds/form/protocols/: vless (auth handlers/state as props), http + mixed (shared accounts-list), hysteria. Drop now-unused HysteriaMasqueradeForm/Typography/Text imports from the modal. InboundFormModal.tsx 2841 -> 2478. Inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.

* refactor(frontend): move HysteriaMasqueradeForm to lib/xray/forms/transport

The hysteria masquerade form edits streamSettings.hysteriaSettings.masquerade (a transport/stream concept) and is rendered identically by both modals, so it belongs next to FinalMaskForm in lib/xray/forms/transport/ rather than protocols/shared/. Moved the file, updated the transport barrel + both consumers (inbound hysteria protocol form, outbound modal), and removed the now-empty protocols/shared/ folder. Pure relocation; snapshots unchanged, typecheck/lint/build green.

* refactor(frontend): extract inbound transport forms into transport/ folder

Move the six inbound stream-transport blocks (tcp/raw, ws, grpc, xhttp,
httpupgrade, kcp) out of InboundFormModal into presentational components
under inbounds/form/transport/. XhttpForm takes the form instance and
re-derives its mode/obfs/placement watches internally; the rest are
declarative. InboundFormModal drops from 2566 to 2105 lines. No behavior
change — per-protocol field-label snapshots unchanged.

* refactor(frontend): extract inbound security forms into security/ folder

Move the inbound TLS and Reality stream-security blocks out of
InboundFormModal into presentational components under
inbounds/form/security/. The Radio.Group security selector stays in the
modal; TlsForm and RealityForm receive their cert/key/ECH generation
handlers and the saving flag as props. InboundFormModal drops from 2105
to 1708 lines.

Add inbound-form-blocks.test.tsx: render-snapshot coverage for each
extracted transport (raw/ws/grpc/kcp/httpupgrade/xhttp) and security
(tls/reality) component in isolation inside a minimal Form. The full
modal cannot exercise the stream/security tabs in jsdom because they are
gated behind Form.useWatch values that do not propagate in the test
harness, so component-level snapshots are the regression net for these
blocks. No behavior change.

* refactor(frontend): extract outbound sockopt/mux/hysteria transport blocks

Move the last three oversized inline stream blocks out of
OutboundFormModal into presentational components under
xray/outbounds/transport/: SockoptForm (~260 lines, the worst offender),
MuxForm, and HysteriaForm. Each takes the form instance; MuxForm also
takes protocol/network and keeps its isMuxAllowed gate. OutboundFormModal
drops from 962 to 621 lines and no inline section now exceeds the
250-line guideline. Existing outbound-form-modal snapshots already cover
sockopt/mux and stay byte-identical, confirming no behavior change.

* refactor(frontend): extract inbound sockopt + external-proxy blocks

Move the inbound Sockopt (~250 lines) and External Proxy stream blocks
out of InboundFormModal into presentational components under
inbounds/form/transport/, mirroring the outbound extraction. Each takes
its toggle handler (toggleSockopt / toggleExternalProxy) as a prop and
keeps its render-prop getFieldValue gate. InboundFormModal drops from
1708 to 1332 lines.

Extend inbound-form-blocks.test.tsx with isolated render-snapshot
coverage for both (SockoptForm seeded enabled + happyEyeballs;
ExternalProxyForm seeded with one TLS entry). No behavior change.

* refactor(frontend): break down RoutingTab into sections

Extract RoutingTab's presentational pieces into the routing/ folder:
helpers.ts (arrJoin/csv/chipPreview/ruleCriteriaChips), types.ts
(RuleRow), CriterionRow.tsx, RuleCardList.tsx (mobile card view), and
useRoutingColumns.tsx (desktop table columns hook). RoutingTab stays the
orchestrator holding rule state, mutate, tag-option memos and the
pointer-drag reorder logic, and drops from 550 to 291 lines. No behavior
change.

* refactor(frontend): extract BasicsTab constants and rule helpers

Move BasicsTab's geo option arrays + freedom/ipv4 outbound presets into
basics/constants.ts and the routing-rule get/set/sync helpers into
basics/helpers.ts. BasicsTab drops from 550 to 447 lines and keeps its
Collapse-of-settings panels (which stay coupled to mutate + derived
state, so splitting them into components would only add prop-drilling).
No behavior change.

* refactor(frontend): break down DnsTab columns/helpers/types

Extract DnsTab's pure pieces into the dns/ folder: helpers.ts
(STRATEGIES/DEFAULT_FAKEDNS + addr/domains/expectedIPs accessors),
types.ts (DnsConfig/HostRow/FakednsRow), and useDnsColumns.tsx
(useDnsServerColumns + useFakednsColumns table-column hooks taking their
row handlers as params). DnsTab stays the orchestrator for dns state,
mutate, hosts sync and the Collapse panels, and drops from 539 to 424
lines. No behavior change.

* refactor(frontend): break down OutboundsTab into sections

Extract OutboundsTab's pieces: outbounds-tab-types.ts (OutboundRow),
outbounds-tab-helpers.ts (address/untestable/security/breakdown +
traffic/testing/result accessors), useOutboundColumns.tsx (desktop table
columns hook) and OutboundCardList.tsx (mobile card view). OutboundsTab
stays the orchestrator for outbound state, mutate, reorder and the
toolbar, and drops from 516 to 238 lines. No behavior change.

This completes plan section 2.4.5 — all four oversized Xray tabs
(Basics/Routing/Dns/Outbounds) are now broken into sections + hooks.

* refactor(frontend): fold HysteriaMasqueradeForm into the hysteria forms

Inline the masquerade fields directly into both hysteria transport forms
(inbounds/form/protocols/hysteria + xray/outbounds/transport/hysteria)
and delete the shared lib/xray/forms/transport/HysteriaMasqueradeForm so
each hysteria form is self-contained. The masquerade JSX is unchanged;
form is typed as the untyped FormInstance (as the shared component was)
so the masquerade name paths still resolve. No behavior change.

* refactor(frontend): slim InboundFormModal by extracting hooks + sections

Pull the modal's non-layout logic into focused files at the form root:
- useSecurityActions.ts: TLS/Reality key + cert generation handlers and
  onSecurityChange (consumed by the security tab)
- useInboundFallbacks.ts: fallback row state + load/save/derive/add/
  update/remove/move handlers + eligible-child options
- FallbacksCard.tsx: the fallbacks card UI (presentational)
- SniffingTab.tsx: the sniffing tab UI (presentational)

Also drop the stale "Pattern A rewrite / sibling file" header comment and
the imports the extractions made unused. InboundFormModal goes from 1332
to 868 lines with no behavior change (351 tests green, snapshots
unchanged).
2026-05-30 21:51:33 +02:00
MHSanaei ffe661d212 fix(groups): fetch full client list for Add/Remove/SubLinks modals
GroupsPage was sourcing modal candidates from useClients(), which is server-paginated at 25 rows — so "Add clients to group" only ever offered the first page, "Remove" missed members past page 1, and SubLinks silently skipped emails whose record wasn't in the cached page. Pull the unpaginated list via /panel/api/clients/list when any of the three modals open.
2026-05-28 20:25:37 +02:00
MHSanaei 72b97efa8a i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider
text, modal okText/cancelText, and Spin loading from the panel pages
(clients/groups/inbounds/nodes/settings/xray/sub/index) into
web/translation/en-US.json under existing pages.<page>.* namespaces, with
JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI,
NordVPN, Cloudflare WARP, etc.) stay literal.

Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP,
pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's
structure and translate the 521 new key paths per locale. Every locale
file now has 1539 lines, mirroring en-US ordering.

Also remove a dead duplicate "info": "Info" key under pages.inbounds
that collided with the new pages.inbounds.info.* object.

Backend: bulk attach/detach errors in web/service/client.go now route
through logger.Warningf (so they appear under /panel/api/server/logs/)
instead of only living on the response payload.
2026-05-28 18:03:07 +02:00
MHSanaei 2fea71387b fix(ui): polish across routing, groups, inbounds, mobile sidebar
A bundle of small UI fixes that surfaced together while reviewing the
panel.

Routing rules — stale Edit after drag:
- Dragging a rule and then clicking its Edit button used to open the
  modal with the *previous* rule's content. Root cause: desktopColumns
  was memoized with [t, isMobile, rows.length] (rows.length doesn't
  change on reorder), so the cached render function kept handing AntD
  the openEdit closure that captured the pre-drag rules array. Fix is
  a rulesRef updated each render and read inside openEdit, so even the
  cached closure sees the live array.
- Mobile rule cards on the same page were hard to tell apart: bumped
  the inter-card gap, slightly stronger border, soft shadow, and a
  small centered divider line between adjacent cards.

Mobile drawer (dark / ultra):
- The AntD Menu inside the mobile drawer was rendering with its own
  darkItemBg (#15161a / #050507) while the drawer body used the
  lighter colorBgElevated, producing visible two-tone seams. Force
  the drawer-content / drawer-body to the same dark color that the
  desktop sider uses, and make the menus transparent so they inherit.

Row menus — visual grouping:
- Groups page row menu: moved Rename above the divider so the
  ordering reads safe → divider → destructive (Remove from group,
  Delete clients, Delete group only) instead of mixing the two
  groups.
- Inbounds page row menu: inserted a divider before delAllClients /
  delete so the destructive items sit visually separated from the
  earlier safe actions.

Dropdown affordances:
- Non-danger dropdown items had no perceivable hover state (default
  colorBgTextHover is too subtle, especially under the light theme).
  Apply the same primary-tint pattern the sider/drawer menu uses: 14%
  primary background and primary color on label + icon.
- ant-dropdown-menu-item-divider now uses var(--ant-color-border)
  (and an explicit rgba in dark) so the separator is actually visible
  in the light theme.

Clients toolbar — narrow-desktop wrap:
- Between 769px and 920px, the bulk-action bar (Attach / Detach /
  Add to group / Ungroup / more + Delete) wrapped to two rows with
  Delete stranded alone on the right. In that range, switch the
  toolbar buttons to icon-only, tighten gap to 6px and inline padding
  to 8px so everything stays on one line.
2026-05-28 13:25:43 +02:00
MHSanaei 530e338c66 refactor(clients): coherent group management — rename, split, extract
This bundles a set of group-related improvements that built up across
one session and only make sense together.

Terminology / API surface:
- Rename "assign group" → "add to group" everywhere: i18n keys,
  callback names (bulkAddToGroup), component + file names
  (BulkAddToGroupModal, AddClientsToGroupModal), Go controller/struct
  names (bulkAddToGroup, AddToGroup), OpenAPI summaries. Nothing keeps
  the word "assign" anymore.
- Move group routes under /panel/api/clients/groups/* (was
  /bulkAssignGroup at the clients root).
- Split add and remove into two endpoints: /groups/bulkAdd now rejects
  empty group; new /groups/bulkRemove clears the label for the given
  emails. The old "submit empty to clear" UX is gone — Ungroup is its
  own action.

UI affordances on Clients page:
- Promote Group + Ungroup to visible bar buttons next to Attach +
  Detach. Group reuses BulkAddToGroupModal; Ungroup pops a danger
  confirm and calls bulkRemoveFromGroup.
- Custom UngroupIcon (TagsOutlined with a diagonal strike) for the
  Ungroup button so the pairing reads at a glance.
- Hide the Group column when no clients have a group label yet —
  removes a column of em-dashes on fresh installs.

UI on Groups page:
- New per-row Add clients… / Remove clients… actions backed by
  GroupAddClientsModal and GroupRemoveClientsModal: rich client picker
  (email / comment / current group / enable) with search and
  preserveSelectedRowKeys, mirroring the inbounds Attach modal UX.

Controller split:
- Move all /groups/* routes, handlers, and request bodies out of
  web/controller/client.go into a dedicated web/controller/group.go
  (GroupController with leaner clientService + xrayService
  dependencies). URLs are byte-identical because the new controller
  registers on the same parent gin.RouterGroup; api_docs_test.go gets
  a group.go → /panel/api/clients basePath entry so its route
  extraction keeps working.

Invalidation dedup:
- Removing a client from a group on the Groups page used to refetch
  /clients/groups and /clients/onlines three times: once from the
  mutation's onSuccess, once from a redundant invalidate() in the
  page's onSubmit, once from the WebSocket invalidate broadcast that
  the backend fires after every mutation. The manual invalidate() is
  gone, and a small invalidationTracker module lets websocketBridge
  skip WS-driven invalidates that arrive within 1.5s of a local
  invalidate — bringing the refetch count down to one. The WS path
  still works for changes made by another tab or user.
2026-05-28 12:59:20 +02:00
MHSanaei 93eda06878 feat(clients,groups): client groups + sub-links export + dedicated groups page
Persistent client groups
- New ClientGroup model + client_groups table that holds empty
  (placeholder) groups so a user can define a label before any client
  references it. ListGroups merges these with the distinct group_name
  values already stored on clients and reports {name, clientCount}.
- ClientRecord gains group_name column; the model.Client wire shape
  gains a matching `group` JSON field that survives the
  inbound.settings → SyncInbound round-trip.
- Rename/Delete on a group mutates client_groups (rename row / delete
  row) AND propagates to all matching clients in ClientRecord and in
  every owning inbound's settings JSON, all in one transaction.

Bulk operations
- AssignGroup(emails, group) updates clients.group_name + patches each
  affected inbound's settings JSON in one read-modify-write per inbound.
  Empty group clears the label. Auto-creates the client_groups row when
  the user assigns to a brand-new name.
- BulkResetTraffic(emails) loops the existing single-reset path so the
  caller can zero traffic across a whole selection or a whole group.
- EmailsByGroup(name) returns just the email list (used by the groups
  page to fan a single bulk action over every member).

Endpoints (all under /panel/api/clients)
- GET  /groups                         — summaries with counts
- GET  /groups/:name/emails            — emails in a group
- POST /groups/create                  — empty placeholder group
- POST /groups/rename                  — rename (table + clients + JSON)
- POST /groups/delete                  — drop label everywhere (clients survive)
- POST /bulkAssignGroup                — assign N selected clients
- POST /bulkResetTraffic               — reset traffic on a list

Clients page UX
- New Group column (Actions → Client → Group → Inbounds → …) with a
  click-to-filter chip.
- FilterDrawer gains a multi-select Group filter whose options come
  from the new ClientPageResponse.groups field (sourced from ListGroups
  so empty/placeholder groups are pickable too).
- Single-client and bulk-add forms gain a Group AutoComplete pre-loaded
  with all known group names.
- New toolbar buttons when selection > 0: "Group ({n})" opens
  BulkAssignGroupModal, "Sub links ({n})" opens SubLinksModal.

Sub-links export modal (new SubLinksModal.tsx)
- Table of selected clients with their subscription URL (and JSON URL
  when subJsonEnable is on), per-row copy, Copy all, and Download as
  sub-links-<timestamp>.txt. Warns when subscription is disabled or
  none of the selected clients have a subId.

Dedicated Groups page (new pages/groups/GroupsPage.tsx)
- /groups route + sidebar entry (TagsOutlined icon) + page title key.
- Card-based layout matching Clients/Inbounds/Nodes — summary card with
  Total/Grouped/Empty stats, main card with Add Group button + table.
- Per-row More dropdown (icon-first column on the left): Sub links,
  Adjust (days+traffic), Reset traffic, Rename, Delete clients in
  group, Delete group (keep clients). Empty groups disable the
  client-targeted actions.
- Reuses SubLinksModal and ClientBulkAdjustModal — emails for the
  group are fetched on demand from GET /groups/:name/emails.

Other polish
- /groups + groups-page selectors added to page-shell.css and
  page-cards.css so the new page inherits the same background, padding,
  card borders, hover shadow, and summary-card padding.
- .card-toolbar gains a small vertical padding so the larger toolbar
  buttons (now default size, matching Inbounds) don't crowd the top of
  the card-head on Clients and Groups pages.
2026-05-27 17:30:55 +02:00