Browse Source

refactor(inbounds): drop the single-user shadowsocks inbound paths

isSSMultiUser treated 2022-blake3-chacha20-poly1305 as a single-user
inbound, and the inbounds page grew a row QR modal, a separate link
panel in the info modal and a client-count skip for that case. The
method is no longer allowed on inbounds, so every shadowsocks inbound
carries clients and those paths cannot be reached: remove them along
with the inbound-level QrCodeModal they alone opened.
MHSanaei 21 hours ago
parent
commit
c20082d254

+ 2 - 5
frontend/src/lib/xray/inbound-link.ts

@@ -1387,11 +1387,8 @@ export function preferPublicHost(browserHost: string, publicHost: string): strin
   return publicHost && isLoopbackHost(browserHost) ? publicHost : browserHost;
 }
 
-// Returns the client array for protocols that have one. SS returns its
-// clients only in 2022-blake3 multi-user mode (matches the legacy
-// `this.clients` getter, which used isSSMultiUser to gate). Returns null
-// for SS single-user, http, mixed, tunnel, wireguard, hysteria2-without-
-// clients, and any protocol without a clients array.
+// Returns the client array for protocols that have one; null for http, mixed,
+// tunnel, wireguard, hysteria2-without-clients, and any protocol without one.
 type ClientShape = {
   id?: string;
   uuid?: string;

+ 1 - 11
frontend/src/lib/xray/protocol-capabilities.ts

@@ -1,6 +1,6 @@
 // Pure-function ports of the legacy Inbound class capability predicates
 // (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
-// canEnableVisionSeed, isSS2022, isSSMultiUser). Each accepts the minimal
+// canEnableVisionSeed, isSS2022). Each accepts the minimal
 // slice of an InboundFormValues it needs, so the same predicate can be
 // called against a partial-row, a full form value, or a hand-built test
 // fixture without the caller projecting a whole object.
@@ -21,7 +21,6 @@ const STREAM_PROTOCOLS = [
 ];
 const VISION_FLOW = 'xtls-rprx-vision';
 const SS_2022_PREFIX = '2022';
-const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
 
 export interface CapabilityProtocolSlice {
   protocol: string;
@@ -103,15 +102,6 @@ export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
   return clients.some((c) => c?.flow === VISION_FLOW);
 }
 
-// Why: legacy returns true on non-SS protocols too (the method getter
-// resolves to "" and "" !== blake3-chacha20-poly1305). Preserved for
-// parity with the legacy class; in practice the callers all narrow on
-// protocol === shadowsocks before checking.
-export function isSSMultiUser(values: CapabilityShadowsocksSlice): boolean {
-  const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
-  return method !== SS_BLAKE3_CHACHA20;
-}
-
 export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
   const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
   return method.substring(0, 4) === SS_2022_PREFIX;

+ 0 - 22
frontend/src/pages/inbounds/InboundsPage.tsx

@@ -51,7 +51,6 @@ import { LazyMount } from '@/components/utility';
 const InboundFormModal = lazy(() => import('./form/InboundFormModal'));
 const CloneInboundModal = lazy(() => import('./CloneInboundModal'));
 const InboundInfoModal = lazy(() => import('./info/InboundInfoModal'));
-const QrCodeModal = lazy(() => import('./qr/QrCodeModal'));
 const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
 const AttachExistingClientsModal = lazy(() => import('./clients/AttachExistingClientsModal'));
 const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
@@ -60,7 +59,6 @@ const AddClientsToGroupModal = lazy(() => import('./clients/AddClientsToGroupMod
 type RowAction =
   | 'edit'
   | 'showInfo'
-  | 'qrcode'
   | 'export'
   | 'subs'
   | 'clipboard'
@@ -167,9 +165,6 @@ export default function InboundsPage() {
   const [infoDbInbound, setInfoDbInbound] = useState<DBInbound | null>(null);
   const [infoClientIndex, setInfoClientIndex] = useState(0);
 
-  const [qrOpen, setQrOpen] = useState(false);
-  const [qrDbInbound, setQrDbInbound] = useState<DBInbound | null>(null);
-
   const [attachOpen, setAttachOpen] = useState(false);
   const [attachSource, setAttachSource] = useState<DBInbound | null>(null);
   const [attachExistingOpen, setAttachExistingOpen] = useState(false);
@@ -213,7 +208,6 @@ export default function InboundsPage() {
     () => hostOverrideFor(infoDbInbound),
     [infoDbInbound, hostOverrideFor],
   );
-  const qrNodeAddress = useMemo(() => hostOverrideFor(qrDbInbound), [qrDbInbound, hostOverrideFor]);
 
   const openText = useCallback(
     (opts: {
@@ -637,7 +631,6 @@ export default function InboundsPage() {
       const hydratingKeys: RowAction[] = [
         'edit',
         'showInfo',
-        'qrcode',
         'export',
         'subs',
         'clipboard',
@@ -659,10 +652,6 @@ export default function InboundsPage() {
           setInfoClientIndex(findClientIndex(target, null));
           setInfoOpen(true);
           break;
-        case 'qrcode':
-          setQrDbInbound(checkFallback(target));
-          setQrOpen(true);
-          break;
         case 'export':
           exportInboundLinks(target);
           break;
@@ -847,17 +836,6 @@ export default function InboundsPage() {
             nodeAddress={infoNodeAddress}
           />
         </LazyMount>
-        <LazyMount when={qrOpen}>
-          <QrCodeModal
-            open={qrOpen}
-            onClose={() => setQrOpen(false)}
-            dbInbound={qrDbInbound}
-            client={null}
-            nodeAddress={qrNodeAddress}
-            subSettings={subSettings}
-            hosts={hosts}
-          />
-        </LazyMount>
         <LazyMount when={attachOpen}>
           <AttachClientsModal
             open={attachOpen}

+ 0 - 22
frontend/src/pages/inbounds/info/InboundInfoModal.tsx

@@ -1302,28 +1302,6 @@ export default function InboundInfoModal({
           ))}
         </>
       )}
-
-      {dbInbound.isSS && !inbound.isSSMultiUser && links.length > 0 && (
-        <>
-          <Divider>{t('pages.inbounds.copyLink')}</Divider>
-          {links.map((link, idx) => (
-            <div key={idx} className="link-panel">
-              <div className="link-panel-header">
-                <Tag color="green">{link.remark || `Link ${idx + 1}`}</Tag>
-                <Tooltip title={t('copy')}>
-                  <Button
-                    size="small"
-                    icon={<CopyOutlined />}
-                    aria-label={t('copy')}
-                    onClick={() => copyText(link.link, t)}
-                  />
-                </Tooltip>
-              </div>
-              <code className="link-panel-text">{link.link}</code>
-            </div>
-          ))}
-        </>
-      )}
     </>
   );
 

+ 1 - 9
frontend/src/pages/inbounds/info/helpers.ts

@@ -2,11 +2,7 @@ import { getMessage } from '@/utils/messageBus';
 import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
 import { Protocols } from '@/schemas/primitives';
 import { coerceInboundJsonField } from '@/models/dbinbound';
-import {
-  canEnableTlsFlow,
-  isSS2022 as isSS2022Helper,
-  isSSMultiUser as isSSMultiUserHelper,
-} from '@/lib/xray/protocol-capabilities';
+import { canEnableTlsFlow, isSS2022 as isSS2022Helper } from '@/lib/xray/protocol-capabilities';
 
 import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
 
@@ -123,10 +119,6 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
     isHttpupgrade: network === 'httpupgrade',
     isXHTTP: network === 'xhttp',
     isGrpc: network === 'grpc',
-    isSSMultiUser: isSSMultiUserHelper({
-      protocol: dbInbound.protocol,
-      settings: settings as { method?: string },
-    }),
     isSS2022: isSS2022Helper({
       protocol: dbInbound.protocol,
       settings: settings as { method?: string },

+ 0 - 1
frontend/src/pages/inbounds/info/types.ts

@@ -36,7 +36,6 @@ export interface InboundInfo {
   isHttpupgrade: boolean;
   isXHTTP: boolean;
   isGrpc: boolean;
-  isSSMultiUser: boolean;
   isSS2022: boolean;
   isVlessTlsFlow: boolean;
   host: string | null;

+ 1 - 5
frontend/src/pages/inbounds/list/RowActions.tsx

@@ -3,7 +3,6 @@ import { Button, Dropdown, type MenuProps } from 'antd';
 import {
   MoreOutlined,
   EditOutlined,
-  QrcodeOutlined,
   CopyOutlined,
   ExportOutlined,
   RetweetOutlined,
@@ -15,7 +14,7 @@ import {
   UsergroupDeleteOutlined,
 } from '@ant-design/icons';
 
-import { isInboundMultiUser, showQrCodeMenu } from './helpers';
+import { isInboundMultiUser } from './helpers';
 import type { DBInboundRecord, RowAction } from './types';
 
 interface RowActionsMenuProps {
@@ -43,9 +42,6 @@ export function buildRowActionsMenu({
   if (isMobile) {
     items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
   }
-  if (showQrCodeMenu(record)) {
-    items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
-  }
   if (isInboundMultiUser(record)) {
     items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
     if (subEnable) {

+ 3 - 12
frontend/src/pages/inbounds/list/helpers.ts

@@ -1,8 +1,7 @@
-import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
 import { coerceInboundJsonField } from '@/models/dbinbound';
 import type { HostRecord } from '@/schemas/api/host';
 
-import type { DBInboundRecord, StreamHints } from './types';
+import type { StreamHints } from './types';
 
 export function readStreamHints(streamSettings: unknown): StreamHints {
   const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
@@ -82,11 +81,12 @@ export function readSettings(settings: unknown): {
   };
 }
 
-export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
+export function isInboundMultiUser(record: { protocol: string }): boolean {
   switch (record.protocol) {
     case 'vmess':
     case 'vless':
     case 'trojan':
+    case 'shadowsocks':
     case 'hysteria':
     case 'mtproto':
     case 'wireguard':
@@ -94,20 +94,11 @@ export function isInboundMultiUser(record: { protocol: string; settings: unknown
     case 'tuic':
     case 'masque':
       return true;
-    case 'shadowsocks':
-      return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
     default:
       return false;
   }
 }
 
-export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
-  if (dbInbound.isSS) {
-    return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
-  }
-  return false;
-}
-
 /** Max host remarks shown inline before truncating with "+N". */
 export const HOST_REMARK_VISIBLE_LIMIT = 2;
 

+ 0 - 1
frontend/src/pages/inbounds/list/types.ts

@@ -55,7 +55,6 @@ export interface InboundSpeedEntry {
 export type RowAction =
   | 'edit'
   | 'showInfo'
-  | 'qrcode'
   | 'export'
   | 'subs'
   | 'clipboard'

+ 0 - 266
frontend/src/pages/inbounds/qr/QrCodeModal.tsx

@@ -1,266 +0,0 @@
-import { useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { Collapse, Modal } from 'antd';
-import type { CollapseProps } from 'antd';
-
-import { Protocols } from '@/schemas/primitives';
-import {
-  genAllLinks,
-  genAmneziaWGPeerConfigs,
-  genAmneziaWGPeerLinks,
-  genWireguardPeerConfigs,
-  genWireguardPeerLinks,
-  isPostQuantumLink,
-  preferPublicHost,
-} from '@/lib/xray/inbound-link';
-import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
-import { withHostEndpoints } from '@/lib/hosts/host-link';
-import type { HostRecord } from '@/schemas/api/host';
-import QrPanel from './QrPanel';
-import { peerConfFileName } from '../info/helpers';
-import type { SubSettings } from '../useInbounds';
-
-interface ClientSetting {
-  email?: string;
-  subId?: string;
-  [k: string]: unknown;
-}
-
-interface QrCodeModalProps {
-  open: boolean;
-  onClose: () => void;
-  dbInbound: (DbInboundLike & { id: number; remark?: string }) | null;
-  client?: ClientSetting | null;
-  nodeAddress?: string;
-  subSettings?: SubSettings;
-  hosts?: HostRecord[];
-}
-
-interface QrItem {
-  key: string;
-  header: string;
-  value: string;
-  downloadName?: string;
-  showQr?: boolean;
-}
-
-const EMPTY_HOSTS: HostRecord[] = [];
-
-export default function QrCodeModal({
-  open,
-  onClose,
-  dbInbound,
-  client = null,
-  nodeAddress = '',
-  subSettings,
-  hosts = EMPTY_HOSTS,
-}: QrCodeModalProps) {
-  const { t } = useTranslation();
-  const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
-  const [wireguardConfigs, setWireguardConfigs] = useState<string[][]>([]);
-  const [wireguardLinks, setWireguardLinks] = useState<string[][]>([]);
-  const [amneziawgConfigs, setAmneziawgConfigs] = useState<string[][]>([]);
-  const [amneziawgLinks, setAmneziawgLinks] = useState<string[][]>([]);
-  const [subLink, setSubLink] = useState('');
-  const [subJsonLink, setSubJsonLink] = useState('');
-  const [activeKey, setActiveKey] = useState<string[]>([]);
-
-  // Building the links is a pure function of the props, so it runs during
-  // render; an effect would paint the previous inbound's QR first.
-  const [syncedProps, setSyncedProps] = useState<{
-    dbInbound: typeof dbInbound;
-    client: typeof client;
-    nodeAddress: typeof nodeAddress;
-    subSettings: typeof subSettings;
-    hosts: typeof hosts;
-  } | null>(null);
-  if (
-    open &&
-    dbInbound &&
-    (syncedProps === null ||
-      syncedProps.dbInbound !== dbInbound ||
-      syncedProps.client !== client ||
-      syncedProps.nodeAddress !== nodeAddress ||
-      syncedProps.subSettings !== subSettings ||
-      syncedProps.hosts !== hosts)
-  ) {
-    setSyncedProps({ dbInbound, client, nodeAddress, subSettings, hosts });
-    const fallbackHostname = preferPublicHost(
-      window.location.hostname,
-      subSettings?.publicHost ?? '',
-    );
-    const inbound = withHostEndpoints(
-      inboundFromDb(dbInbound),
-      dbInbound.id,
-      hosts,
-      nodeAddress,
-      fallbackHostname,
-    );
-    if (inbound.protocol === Protocols.WIREGUARD) {
-      const peerRemark = client?.email
-        ? `${dbInbound.remark}-${client.email}`
-        : dbInbound.remark || '';
-      setWireguardConfigs(
-        genWireguardPeerConfigs({
-          inbound,
-          remark: peerRemark,
-          hostOverride: nodeAddress,
-          fallbackHostname,
-        }),
-      );
-      setWireguardLinks(
-        genWireguardPeerLinks({
-          inbound,
-          remark: peerRemark,
-          hostOverride: nodeAddress,
-          fallbackHostname,
-        }),
-      );
-      setAmneziawgConfigs([]);
-      setAmneziawgLinks([]);
-      setLinks([]);
-    } else if (inbound.protocol === Protocols.AMNEZIAWG) {
-      const peerRemark = client?.email
-        ? `${dbInbound.remark}-${client.email}`
-        : dbInbound.remark || '';
-      setAmneziawgConfigs(
-        genAmneziaWGPeerConfigs({
-          inbound,
-          remark: peerRemark,
-          hostOverride: nodeAddress,
-          fallbackHostname,
-        }),
-      );
-      setAmneziawgLinks(
-        genAmneziaWGPeerLinks({
-          inbound,
-          remark: peerRemark,
-          hostOverride: nodeAddress,
-          fallbackHostname,
-        }),
-      );
-      setWireguardConfigs([]);
-      setWireguardLinks([]);
-      setLinks([]);
-    } else {
-      setLinks(
-        genAllLinks({
-          inbound,
-          remark: dbInbound.remark || '',
-          client: client ?? {},
-          hostOverride: nodeAddress,
-          fallbackHostname,
-        }),
-      );
-      setWireguardConfigs([]);
-      setWireguardLinks([]);
-      setAmneziawgConfigs([]);
-      setAmneziawgLinks([]);
-    }
-
-    const subId = client?.subId;
-    let nextSub = '';
-    let nextSubJson = '';
-    if (subSettings?.enable && subId) {
-      nextSub = (subSettings.subURI || '') + subId;
-      nextSubJson = subSettings.subJsonEnable ? (subSettings.subJsonURI || '') + subId : '';
-    }
-    setSubLink(nextSub);
-    setSubJsonLink(nextSubJson);
-  }
-
-  const qrItems = useMemo<QrItem[]>(() => {
-    const items: QrItem[] = [];
-    if (subLink) {
-      items.push({ key: 'sub', header: t('subscription.title'), value: subLink });
-    }
-    if (subJsonLink) {
-      items.push({
-        key: 'sub-json',
-        header: `${t('subscription.title')} (JSON)`,
-        value: subJsonLink,
-      });
-    }
-    links.forEach((link, idx) => {
-      items.push({ key: `l${idx}`, header: link.remark || `Link ${idx + 1}`, value: link.link });
-    });
-    const pushTunnelPeers = (prefix: string, configs: string[][], peerLinks: string[][]) => {
-      configs.forEach((peerConfigs, idx) => {
-        peerConfigs.forEach((cfg, j) => {
-          const multi = peerConfigs.length > 1 ? ` #${j + 1}` : '';
-          items.push({
-            key: `${prefix}c${idx}-${j}`,
-            header: `Peer ${idx + 1} config${multi}`,
-            value: cfg,
-            downloadName: peerConfFileName(idx, j, peerConfigs.length),
-          });
-          const link = peerLinks[idx]?.[j];
-          if (link) {
-            items.push({
-              key: `${prefix}l${idx}-${j}`,
-              header: `Peer ${idx + 1} link${multi}`,
-              value: link,
-              showQr: false,
-            });
-          }
-        });
-      });
-    };
-    pushTunnelPeers('w', wireguardConfigs, wireguardLinks);
-    pushTunnelPeers('a', amneziawgConfigs, amneziawgLinks);
-    return items;
-  }, [
-    subLink,
-    subJsonLink,
-    links,
-    wireguardConfigs,
-    wireguardLinks,
-    amneziawgConfigs,
-    amneziawgLinks,
-    t,
-  ]);
-
-  const collapseItems: CollapseProps['items'] = useMemo(
-    () =>
-      qrItems.map((item) => ({
-        key: item.key,
-        label: item.header,
-        children: (
-          <QrPanel
-            value={item.value}
-            remark={item.header}
-            downloadName={item.downloadName || ''}
-            showQr={item.showQr !== false && !isPostQuantumLink(item.value)}
-          />
-        ),
-      })),
-    [qrItems],
-  );
-
-  const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null;
-  const [syncedFirstKey, setSyncedFirstKey] = useState<string | null>(null);
-  if (firstKey !== syncedFirstKey) {
-    setSyncedFirstKey(firstKey);
-    setActiveKey(firstKey ? [firstKey] : []);
-  }
-
-  return (
-    <Modal
-      open={open}
-      onCancel={onClose}
-      title={t('qrCode')}
-      footer={null}
-      width={420}
-      destroyOnHidden
-    >
-      {dbInbound && collapseItems && collapseItems.length > 0 && (
-        <Collapse
-          ghost
-          activeKey={activeKey}
-          onChange={(keys) => setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))}
-          items={collapseItems}
-        />
-      )}
-    </Modal>
-  );
-}

+ 0 - 1
frontend/src/pages/inbounds/qr/index.ts

@@ -1,2 +1 @@
 export { default as QrPanel } from './QrPanel';
-export { default as QrCodeModal } from './QrCodeModal';

+ 0 - 3
frontend/src/pages/inbounds/useInbounds.ts

@@ -6,7 +6,6 @@ import { parseMsg } from '@/utils/zodValidate';
 import { DBInbound, coerceInboundJsonField } from '@/models/dbinbound';
 import type { ClientStats, DBInboundInit } from '@/models/dbinbound';
 import { Protocols } from '@/schemas/primitives';
-import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
 import { setDatepicker } from '@/hooks/useDatepicker';
 import { keys } from '@/api/queryKeys';
 import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
@@ -351,10 +350,8 @@ export function useInbounds() {
       const protocol = dbInbound.protocol;
       if (!TRACKED_PROTOCOLS.includes(protocol)) continue;
       const settings = coerceInboundJsonField(dbInbound.settings) as {
-        method?: string;
         clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
       };
-      if (protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol, settings })) continue;
       counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients });
     }
     return counts;

File diff suppressed because it is too large
+ 0 - 126
frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap


+ 1 - 19
frontend/src/test/inbound-from-db.test.ts

@@ -8,7 +8,7 @@ import {
   genWireguardLinks,
   getInboundClients,
 } from '@/lib/xray/inbound-link';
-import { canEnableTlsFlow, isSS2022, isSSMultiUser } from '@/lib/xray/protocol-capabilities';
+import { canEnableTlsFlow, isSS2022 } from '@/lib/xray/protocol-capabilities';
 
 const FALLBACK_HOST = 'panel.example.test';
 
@@ -171,24 +171,6 @@ describe('inboundFromDb', () => {
 });
 
 describe('protocol-capability helpers with raw coerced shapes', () => {
-  it('isSSMultiUser returns true for legacy SS methods', () => {
-    expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(
-      true,
-    );
-    expect(
-      isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),
-    ).toBe(true);
-  });
-
-  it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => {
-    expect(
-      isSSMultiUser({
-        protocol: 'shadowsocks',
-        settings: { method: '2022-blake3-chacha20-poly1305' },
-      }),
-    ).toBe(false);
-  });
-
   it('isSS2022 detects 2022-blake3 family', () => {
     expect(
       isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } }),

+ 0 - 2
frontend/src/test/protocol-capabilities.test.ts

@@ -8,7 +8,6 @@ import {
   canEnableStream,
   canEnableVisionSeed,
   isSS2022,
-  isSSMultiUser,
 } from '@/lib/xray/protocol-capabilities';
 
 // Pure-function tests for the capability predicates. Each fixture × stream
@@ -67,7 +66,6 @@ describe('protocol capability predicates', () => {
           canEnableStream: canEnableStream(values),
           canEnableVisionSeed: canEnableVisionSeed(values),
           isSS2022: isSS2022(values),
-          isSSMultiUser: isSSMultiUser(values),
         };
         expect(result).toMatchSnapshot();
       });

Some files were not shown because too many files changed in this diff