@swimmingliu/autovpn 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,32 @@
1
1
  import { buildStageModel, PAGE_INDEX, PAGE_ORDER, resolveRunControlState } from './state.js';
2
2
  import { icon } from './icons.js';
3
3
 
4
+ // Defaults for a source row that has never stored a value. A round is one
5
+ // cursor step over that brand's fixed area list (one region by default), so the
6
+ // count is per platform and per region: 4/2 would fetch only a handful of nodes.
7
+ export const SOURCE_ROUND_DEFAULTS = {
8
+ max_iterations: 200,
9
+ plateau_limit: 50,
10
+ areas_per_round: 8,
11
+ area_concurrency: 4
12
+ };
13
+ export const SOURCE_ROUND_LIMITS = {
14
+ max_iterations: [1, Number.POSITIVE_INFINITY],
15
+ plateau_limit: [1, Number.POSITIVE_INFINITY],
16
+ areas_per_round: [1, 64],
17
+ area_concurrency: [1, 16]
18
+ };
19
+ export const NETWORK_DEFAULTS = {
20
+ proxy_mode: 'proxy',
21
+ proxy_url: 'http://127.0.0.1:7897'
22
+ };
23
+
4
24
  const FALLBACK_PROFILE = {
5
25
  sources: {
6
- leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true, max_iterations: 4, plateau_limit: 2 },
7
- heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true, max_iterations: 4, plateau_limit: 2 },
8
- mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true, max_iterations: 4, plateau_limit: 2 },
9
- xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true, max_iterations: 4, plateau_limit: 2 }
26
+ leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
27
+ heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
28
+ mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
29
+ xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true, ...SOURCE_ROUND_DEFAULTS }
10
30
  },
11
31
  speed_test: {
12
32
  min_download_mb_s: 1,
@@ -569,6 +589,7 @@ function buildSettingsPage(vm, messages) {
569
589
  ${buildSettingsCard('speed_test', '测速配置', `最低 ${vm.profile.speed_test.min_download_mb_s} MB/s · 并发 ${vm.profile.speed_test.concurrency}`)}
570
590
  ${buildSettingsCard('availability_targets', 'AI可达性检测', `已启用 ${enabledAvailabilityTargetCount(vm.profile.availability_targets)} / ${Object.keys(vm.profile.availability_targets ?? {}).length} 个网站`)}
571
591
  ${buildSettingsCard('deploy', '部署配置', `项目 ${vm.profile.deploy.project_name || '未设置'}`)}
592
+ ${buildSettingsCard('network', '网络出口', networkSummary(vm.profile.network))}
572
593
  </div>
573
594
  </article>
574
595
 
@@ -633,12 +654,24 @@ export function buildAccountPoolRecords(pool = EMPTY_ACCOUNT_POOL) {
633
654
  ${rows.map((account) => {
634
655
  const meta = ACCOUNT_STATUS_META[account.status] ?? { label: account.status, tone: 'neutral' };
635
656
  const retired = account.status !== 'active';
657
+ const expiry = String(account.expires_at ?? '').trim();
658
+ const editingExpiry = pool.canSetExpiry && Number(pool.expiryEditorId) === Number(account.id);
636
659
  return `
637
660
  <tr data-account-row="${account.id}">
638
661
  <td data-field-label="账号"><code class="account-mask">${escapeHtml(account.account_masked)}</code>${account.note ? `<span class="account-note">${escapeHtml(account.note)}</span>` : ''}</td>
639
662
  <td data-field-label="加速器">${escapeHtml(SOURCE_NAMES[account.brand] ?? account.brand)}</td>
640
663
  <td data-field-label="状态">${renderBadge(meta.label, meta.tone)}${account.status_reason ? `<span class="account-note">${escapeHtml(account.status_reason)}</span>` : ''}</td>
641
- <td data-field-label="到期">${escapeHtml(account.expires_at || '—')}</td>
664
+ <td data-field-label="到期">
665
+ <span class="account-expiry-value ${expiry ? '' : 'unknown'}">${escapeHtml(expiry || '未知')}</span>
666
+ ${pool.canSetExpiry ? (editingExpiry
667
+ ? `<div class="account-expiry-editor">
668
+ <input type="date" aria-label="${escapeHtml(account.account_masked)}:到期日期" data-account-expiry-input="${account.id}" value="${escapeHtml(expiry.slice(0, 10))}" ${pool.busy ? 'disabled' : ''} />
669
+ <button class="btn btn-primary small" data-account-action="expiry-save" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>保存</button>
670
+ <button class="btn btn-secondary small" data-account-action="expiry-cancel" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>取消</button>
671
+ </div>`
672
+ : `<button class="btn btn-secondary small account-expiry-edit" data-account-action="expiry-edit" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>${expiry ? '改期' : '设置到期'}</button>`)
673
+ : ''}
674
+ </td>
642
675
  <td data-field-label="使用"><span class="account-usage">${Number(account.use_count ?? 0)} 次</span><span class="account-note">${Number(account.success_count ?? 0)} 成功 / ${Number(account.fail_count ?? 0)} 失败</span></td>
643
676
  <td data-field-label="操作"><button class="btn btn-secondary small ${retired ? '' : 'destructive'}" data-account-action="toggle" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>${retired ? '启用' : '停用'}</button></td>
644
677
  </tr>
@@ -679,6 +712,7 @@ export function buildAccountPoolPanel(vm) {
679
712
  `).join('')}
680
713
  </div>
681
714
  ${Number(stats.expiring_soon ?? 0) > 0 ? `<p class="account-pool-expiry">7 天内到期 ${Number(stats.expiring_soon)} 个</p>` : ''}
715
+ ${Number.isFinite(Number(stats.expiry_unknown)) && Number(stats.expiry_unknown) > 0 ? `<p class="account-pool-expiry unknown">到期时间未知 ${Number(stats.expiry_unknown)}</p>` : ''}
682
716
  </div>
683
717
 
684
718
  <div class="account-pool-toolbar">
@@ -767,7 +801,8 @@ function buildSettingsDrawer(vm) {
767
801
  sources: '数据源配置',
768
802
  speed_test: '测速配置',
769
803
  availability_targets: 'AI可达性检测',
770
- deploy: '部署配置'
804
+ deploy: '部署配置',
805
+ network: '网络出口'
771
806
  }[section] ?? '设置';
772
807
 
773
808
  const style = vm.modalTransform ? `style="transform: ${escapeHtml(vm.modalTransform)};"` : '';
@@ -830,7 +865,11 @@ const EMPTY_ACCOUNT_POOL = {
830
865
  statusFilter: 'all',
831
866
  brandFilter: 'all',
832
867
  sortBy: 'brand',
833
- busy: false
868
+ busy: false,
869
+ // Feature-detected from the preload bridge. The H5/browser build has no
870
+ // setAccountExpiry, so the row simply omits the affordance there.
871
+ canSetExpiry: false,
872
+ expiryEditorId: null
834
873
  };
835
874
 
836
875
  /** Credential sources are also configured by their official site. */
@@ -849,7 +888,13 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
849
888
  <div class="source-drawer-settings">
850
889
  <div class="source-drawer-settings-copy">
851
890
  <strong>每个加速器只填官网</strong>
852
- <span>密钥从 APK 自动发现,账号密码由账号池提供。Android 与 iOS 两个通道同时抓取,每条线路按「轮数」循环请求,连续「早停」轮没有新节点就停止。</span>
891
+ <span>密钥从 APK 自动发现,账号密码由账号池提供。Android 与 iOS 两个通道同时抓取;「轮数」由两端各自独立跑满,1000 就是 Android 1000 轮加 iOS 1000 轮,不是 500+500。</span>
892
+ </div>
893
+ <div class="source-drawer-settings-copy">
894
+ <span>每次运行先按该加速器的节点列表生成一份固定区域列表(去掉中国大陆与免费区域、去重、保持原顺序),运行中不再改变。每一轮用游标依次取出「每轮区域数」个区域(默认 8 个,取完回绕);「区域并发」控制该轮这些区域请求同时发出的数量(默认 4)。31 个区域默认四轮即可覆盖一遍,轮数按各平台独立计算。</span>
895
+ </div>
896
+ <div class="source-drawer-settings-copy">
897
+ <span>「早停」按连续无新增节点的轮数计数。是否为新节点按节点指纹判断(address / port / id / transport / host / path / tls / sni),并在 Android 与 iOS 之间统一去重:改名重复的节点、或另一端已经抓到的节点,都不会让计数归零。</span>
853
898
  </div>
854
899
  </div>
855
900
  </details>
@@ -884,14 +929,22 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
884
929
  <div class="source-credential-rounds">
885
930
  <label class="source-field">
886
931
  <span>轮数</span>
887
- <input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:抓取轮数" title="每条线路循环请求的次数;每轮服务端会返回不同落地机" data-drawer-source="${escapeHtml(name)}" data-drawer-key="max_iterations" value="${escapeHtml(source.max_iterations ?? 4)}" />
932
+ <input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:抓取轮数" title="Android 与 iOS 各自独立跑满该轮数(1000 = 两端各 1000 轮);每轮是固定区域列表上的一个游标步,默认只取 1 个区域" data-drawer-source="${escapeHtml(name)}" data-drawer-key="max_iterations" value="${escapeHtml(source.max_iterations ?? SOURCE_ROUND_DEFAULTS.max_iterations)}" />
888
933
  </label>
889
934
  <label class="source-field">
890
935
  <span>早停</span>
891
- <input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:连续空轮早停次数" title="连续多少轮没有新节点就停止" data-drawer-source="${escapeHtml(name)}" data-drawer-key="plateau_limit" value="${escapeHtml(source.plateau_limit ?? 2)}" />
936
+ <input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:连续空轮早停次数" title="连续多少轮没有真正新增节点就停止;节点指纹在双端之间统一去重" data-drawer-source="${escapeHtml(name)}" data-drawer-key="plateau_limit" value="${escapeHtml(source.plateau_limit ?? SOURCE_ROUND_DEFAULTS.plateau_limit)}" />
937
+ </label>
938
+ <label class="source-field">
939
+ <span>每轮区域数</span>
940
+ <input type="number" min="1" max="64" step="1" inputmode="numeric" aria-label="${label}:每轮区域数" title="每轮从固定区域列表按游标取出的区域数,列表在运行中不变;取完回绕" data-drawer-source="${escapeHtml(name)}" data-drawer-key="areas_per_round" value="${escapeHtml(source.areas_per_round ?? SOURCE_ROUND_DEFAULTS.areas_per_round)}" />
941
+ </label>
942
+ <label class="source-field">
943
+ <span>区域并发</span>
944
+ <input type="number" min="1" max="16" step="1" inputmode="numeric" aria-label="${label}:区域并发" title="该轮区域请求同时发出的数量" data-drawer-source="${escapeHtml(name)}" data-drawer-key="area_concurrency" value="${escapeHtml(source.area_concurrency ?? SOURCE_ROUND_DEFAULTS.area_concurrency)}" />
892
945
  </label>
893
946
  </div>
894
- <span class="source-round-summary">双端并发</span>
947
+ <span class="source-round-summary">双端各自计轮 · 每轮按固定区域列表推进</span>
895
948
  </td>
896
949
  </tr>
897
950
  `;
@@ -965,6 +1018,25 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
965
1018
  `;
966
1019
  }
967
1020
 
1021
+ if (section === 'network') {
1022
+ const network = buildNetworkDraft(draft);
1023
+ return `
1024
+ <details class="context-help">
1025
+ <summary>出口与回退规则</summary>
1026
+ <p>代理:全程经 proxy_url 抓取,代理没在监听时自动改为直连;自动:先直连,遇到 IP 被封(403/429 或封锁页)后整轮改走代理;直连:不使用代理。默认走本机 127.0.0.1:7897。</p>
1027
+ </details>
1028
+ <div class="form-grid compact-form-grid network-form-grid">
1029
+ <label class="field compact">
1030
+ <span>出口模式</span>
1031
+ <select data-drawer-path="network.proxy_mode" aria-label="出口模式">
1032
+ ${[['proxy', '代理'], ['auto', '自动(被封后切代理)'], ['direct', '直连']].map(([value, label]) => `<option value="${value}" ${network.proxy_mode === value ? 'selected' : ''}>${label}</option>`).join('')}
1033
+ </select>
1034
+ </label>
1035
+ ${renderDrawerField('代理地址', 'text', network.proxy_url, 'network.proxy_url', true, 'inputmode="url" spellcheck="false" placeholder="http://127.0.0.1:7897"')}
1036
+ </div>
1037
+ `;
1038
+ }
1039
+
968
1040
  return `
969
1041
  <div class="notice-card">
970
1042
  <strong>无可用配置</strong>
@@ -973,6 +1045,29 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
973
1045
  `;
974
1046
  }
975
1047
 
1048
+ export function buildNetworkDraft(network = {}) {
1049
+ const mode = String(network?.proxy_mode ?? '').trim().toLowerCase();
1050
+ const url = String(network?.proxy_url ?? '').trim();
1051
+ return {
1052
+ proxy_mode: ['proxy', 'direct', 'auto'].includes(mode) ? mode : NETWORK_DEFAULTS.proxy_mode,
1053
+ proxy_url: url || NETWORK_DEFAULTS.proxy_url
1054
+ };
1055
+ }
1056
+
1057
+ export function applyNetworkDraft(draft = {}) {
1058
+ const network = buildNetworkDraft(draft);
1059
+ return {
1060
+ proxy_mode: network.proxy_mode,
1061
+ proxy_url: network.proxy_url
1062
+ };
1063
+ }
1064
+
1065
+ function networkSummary(network = {}) {
1066
+ const resolved = buildNetworkDraft(network);
1067
+ const modeLabel = { proxy: '代理', auto: '自动', direct: '直连' }[resolved.proxy_mode];
1068
+ return resolved.proxy_mode === 'direct' ? modeLabel : `${modeLabel} ${resolved.proxy_url}`;
1069
+ }
1070
+
976
1071
  function formatAvailabilityName(name) {
977
1072
  const labels = { gemini: 'Gemini', chatgpt: 'ChatGPT', claude: 'Claude' };
978
1073
  return labels[String(name ?? '').toLowerCase()] ?? String(name || '网站');
@@ -1057,8 +1152,10 @@ function buildSourceRows(profile) {
1057
1152
  enabled: source.enabled !== false,
1058
1153
  mode: 'credential',
1059
1154
  officialSite: source.official_site ?? '',
1060
- maxIterations: Number(source.max_iterations ?? 4),
1061
- plateauLimit: Number(source.plateau_limit ?? 2)
1155
+ maxIterations: Number(source.max_iterations ?? SOURCE_ROUND_DEFAULTS.max_iterations),
1156
+ plateauLimit: Number(source.plateau_limit ?? SOURCE_ROUND_DEFAULTS.plateau_limit),
1157
+ areasPerRound: Number(source.areas_per_round ?? SOURCE_ROUND_DEFAULTS.areas_per_round),
1158
+ areaConcurrency: Number(source.area_concurrency ?? SOURCE_ROUND_DEFAULTS.area_concurrency)
1062
1159
  }));
1063
1160
  }
1064
1161
 
@@ -1122,8 +1219,10 @@ export function buildSourceIterationDraft(sources = {}) {
1122
1219
  const firstSource = Object.values(normalized)[0] ?? {};
1123
1220
  return {
1124
1221
  sources: normalized,
1125
- maxIterations: coercePositiveInteger(firstSource.max_iterations, 4),
1126
- plateauLimit: coercePositiveInteger(firstSource.plateau_limit, 2)
1222
+ maxIterations: coercePositiveInteger(firstSource.max_iterations, SOURCE_ROUND_DEFAULTS.max_iterations),
1223
+ plateauLimit: coercePositiveInteger(firstSource.plateau_limit, SOURCE_ROUND_DEFAULTS.plateau_limit),
1224
+ areasPerRound: coerceBoundedInteger(firstSource.areas_per_round, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64),
1225
+ areaConcurrency: coerceBoundedInteger(firstSource.area_concurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16)
1127
1226
  };
1128
1227
  }
1129
1228
 
@@ -1186,13 +1285,21 @@ export function applySourceIterationDraft(sources = {}, draft = {}) {
1186
1285
  if (draft?.sources) {
1187
1286
  return normalizeOfficialSourceDraft(draft.sources);
1188
1287
  }
1189
- const maxIterations = coercePositiveInteger(draft.maxIterations, 4);
1190
- const plateauLimit = coercePositiveInteger(draft.plateauLimit, 2);
1288
+ const maxIterations = coercePositiveInteger(draft.maxIterations, SOURCE_ROUND_DEFAULTS.max_iterations);
1289
+ const plateauLimit = coercePositiveInteger(draft.plateauLimit, SOURCE_ROUND_DEFAULTS.plateau_limit);
1290
+ const areasPerRound = coerceBoundedInteger(draft.areasPerRound, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64);
1291
+ const areaConcurrency = coerceBoundedInteger(draft.areaConcurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16);
1191
1292
  const normalized = normalizeOfficialSourceDraft(sources);
1192
1293
  return Object.fromEntries(
1193
1294
  Object.entries(normalized).map(([name, source]) => [
1194
1295
  name,
1195
- { ...source, max_iterations: maxIterations, plateau_limit: plateauLimit }
1296
+ {
1297
+ ...source,
1298
+ max_iterations: maxIterations,
1299
+ plateau_limit: plateauLimit,
1300
+ areas_per_round: areasPerRound,
1301
+ area_concurrency: areaConcurrency
1302
+ }
1196
1303
  ])
1197
1304
  );
1198
1305
  }
@@ -1222,6 +1329,20 @@ export function validateSourceDraft(draft = {}) {
1222
1329
  if (!Number.isFinite(Number(draft.plateauLimit)) || Number(draft.plateauLimit) < 1) {
1223
1330
  return '早停轮数至少为 1';
1224
1331
  }
1332
+ for (const [name, source] of Object.entries(sources)) {
1333
+ if (!SOURCE_DISPLAY_ORDER.includes(name)) continue;
1334
+ for (const [key, label, [, max]] of [
1335
+ ['areas_per_round', '每轮区域数', SOURCE_ROUND_LIMITS.areas_per_round],
1336
+ ['area_concurrency', '区域并发', SOURCE_ROUND_LIMITS.area_concurrency]
1337
+ ]) {
1338
+ const raw = source?.[key];
1339
+ if (raw === undefined || raw === null || raw === '') continue;
1340
+ const value = Number(raw);
1341
+ if (!Number.isFinite(value) || value < 1 || value > max) {
1342
+ return `${SOURCE_NAMES[name] ?? name}:${label}必须是 1-${max} 之间的整数`;
1343
+ }
1344
+ }
1345
+ }
1225
1346
  return '';
1226
1347
  }
1227
1348
 
@@ -1239,6 +1360,16 @@ function coercePositiveInteger(value, fallback) {
1239
1360
  return Math.trunc(parsed);
1240
1361
  }
1241
1362
 
1363
+ // The two area controls have a closed range; a stored value from outside it is
1364
+ // clamped rather than rejected, so an older or hand-edited profile still opens.
1365
+ function coerceBoundedInteger(value, fallback, min, max) {
1366
+ const parsed = Number(value);
1367
+ if (!Number.isFinite(parsed)) {
1368
+ return fallback;
1369
+ }
1370
+ return Math.min(max, Math.max(min, Math.trunc(parsed)));
1371
+ }
1372
+
1242
1373
  function coerceAreaValue(value, fallback) {
1243
1374
  const parsed = Number(value);
1244
1375
  if (!Number.isFinite(parsed)) {
@@ -1374,6 +1505,8 @@ export function normalizeOfficialSourceDraft(sources = {}) {
1374
1505
  // not overwrite them just by appearing later in the file.
1375
1506
  max_iterations: existing.max_iterations ?? source.max_iterations,
1376
1507
  plateau_limit: existing.plateau_limit ?? source.plateau_limit,
1508
+ areas_per_round: existing.areas_per_round ?? source.areas_per_round,
1509
+ area_concurrency: existing.area_concurrency ?? source.area_concurrency,
1377
1510
  enabled: existing.enabled !== false || source.enabled !== false
1378
1511
  }
1379
1512
  : { ...source };
@@ -1391,8 +1524,10 @@ export function normalizeOfficialSourceDraft(sources = {}) {
1391
1524
  ? (OFFICIAL_SITES[name] ?? '')
1392
1525
  : String(source.official_site).trim(),
1393
1526
  enabled: source.enabled !== false,
1394
- max_iterations: wasCredential ? coercePositiveInteger(source.max_iterations, 4) : 4,
1395
- plateau_limit: wasCredential ? coercePositiveInteger(source.plateau_limit, 2) : 2
1527
+ max_iterations: wasCredential ? coercePositiveInteger(source.max_iterations, SOURCE_ROUND_DEFAULTS.max_iterations) : SOURCE_ROUND_DEFAULTS.max_iterations,
1528
+ plateau_limit: wasCredential ? coercePositiveInteger(source.plateau_limit, SOURCE_ROUND_DEFAULTS.plateau_limit) : SOURCE_ROUND_DEFAULTS.plateau_limit,
1529
+ areas_per_round: wasCredential ? coerceBoundedInteger(source.areas_per_round, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64) : SOURCE_ROUND_DEFAULTS.areas_per_round,
1530
+ area_concurrency: wasCredential ? coerceBoundedInteger(source.area_concurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16) : SOURCE_ROUND_DEFAULTS.area_concurrency
1396
1531
  };
1397
1532
  for (const field of OBSOLETE_SOURCE_FIELDS) delete normalized[field];
1398
1533
  // A URL-mode floor is the same mismatch as its URL-mode cap: 10 URL fetches
@@ -1419,6 +1554,7 @@ function mergeProfile(profile) {
1419
1554
  availability_targets: profile?.availability_targets ?? DEFAULT_AVAILABILITY_TARGETS,
1420
1555
  speed_test: buildSpeedTestDraft(profile?.speed_test),
1421
1556
  deploy: { ...FALLBACK_PROFILE.deploy, ...(profile?.deploy ?? {}) },
1557
+ network: buildNetworkDraft(profile?.network),
1422
1558
  paths: mergedPaths,
1423
1559
  workspace: mergedPaths
1424
1560
  };
@@ -203,6 +203,10 @@ body[data-page="logs"] #logsWorkspace > .panel { grid-template-rows: auto minmax
203
203
  .account-pool-toolbar input, .account-pool-toolbar select { min-height: 36px; }
204
204
  .settings-drawer-panel { width: min(800px, calc(100vw - 48px)); max-width: 800px; padding: 24px; border-radius: 14px; background: var(--surface); }
205
205
  .settings-drawer-shell[data-section="speed_test"] .settings-drawer-panel { width: min(560px, calc(100vw - 48px)); }
206
+ .settings-drawer-shell[data-section="network"] .settings-drawer-panel { width: min(620px, calc(100vw - 48px)); }
207
+ .settings-drawer-shell[data-section="network"] .network-form-grid { grid-template-columns: minmax(0, 220px) minmax(0, 1fr); align-items: end; }
208
+ .settings-drawer-shell[data-section="network"] .field select { width: 100%; min-height: 36px; }
209
+ .settings-drawer-shell[data-section="network"] .field input { width: 100%; max-width: 100%; min-height: 36px; }
206
210
  .settings-drawer-head h3 { font-size: 19px; margin: 4px 0; letter-spacing: -.02em; }
207
211
  .settings-drawer-shell[data-section="speed_test"] .compact-form-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
208
212
  .settings-drawer-shell[data-section="speed_test"] .field input { width: 100%; max-width: 100%; min-height: 36px; }
@@ -351,6 +355,7 @@ body[data-page="logs"] #logsWorkspace > .panel { grid-template-rows: auto minmax
351
355
  .settings-drawer-panel, .settings-drawer-shell[data-section] .settings-drawer-panel { width: 100vw; max-width: none; padding: 0; border-radius: 0; background: var(--surface); }
352
356
  .settings-drawer-actions { background: var(--surface); }
353
357
  .settings-drawer-shell[data-section="speed_test"] .compact-form-grid { grid-template-columns: 1fr; }
358
+ .settings-drawer-shell[data-section="network"] .network-form-grid { grid-template-columns: 1fr; }
354
359
  .settings-drawer-shell[data-section="deploy"] .compact-form-grid { grid-template-columns: 1fr; }
355
360
  .settings-drawer-shell[data-section="speed_test"] .field input { width: 100%; min-height: 44px; }
356
361
  .field-wide { grid-column: auto; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmingliu/autovpn",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "npm wrapper for the AutoVPN headless CLI",
5
5
  "type": "module",
6
6
  "repository": {