dsh-best-ip 1.1.2 → 1.2.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.
Files changed (5) hide show
  1. package/README.md +17 -6
  2. package/bench.js +177 -57
  3. package/client.js +200 -88
  4. package/index.js +280 -62
  5. package/package.json +1 -1
package/client.js CHANGED
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
31
31
  autoApplyTitle: 'Apply the recommended address after successful testing. Keep the current address on failure.',
32
32
  speedTest: 'Automatic speed test',
33
33
  speedTestHint:
34
- 'Discover usable addresses, then test the lowest-ping IPs up to the selected limit (all by default), equally with 3 runs by default. Rank by conservative speed, variability and reliable sample rate.',
34
+ 'Discover addresses, reserve one slot for the previously selected IP, then fill the remaining slots by lowest ping (all by default) over 3 rounds by default. Rank by capped harmonic speed, lower-quartile speed, actual success rate and measurement confidence; skipped requests do not count as failures.',
35
35
  maxBenchmarkIps: 'Maximum test IPs',
36
36
  benchmarkIpLimitAll: 'All',
37
37
  benchmarkIpLimitCustom: 'Custom',
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
42
42
  benchmarkCostLimitSummary: '{count} IPs: up to about {compactTotalEn} tokens',
43
43
  benchmarkCostDetails: 'Token budget details',
44
44
  benchmarkCostHint: 'Budget for {count} usable IPs × {runs} runs/IP × up to {tokens} output tokens/run: about {total} output tokens, plus input tokens. Actual usage depends on usable IPs and output length.',
45
- benchmarkCostAllHint: '“All” is estimated using the extra scan budget of {count} IPs. DNS answers are additional, so the actual IP count may be higher.',
45
+ benchmarkCostAllHint: '“All” is estimated using the extra scan budget of {count} IPs. DNS answers and a previously selected IP are additional to the scan budget, so the actual IP count may be higher.',
46
46
  stepProbe: 'Discovering addresses',
47
47
  stepBenchmark: 'Timing throughput',
48
48
  stepConfirm: 'Repeating measurements',
@@ -83,22 +83,45 @@ window.__ModuleLoader__.load({
83
83
  defaultChangedHint: 'The DSH default provider/model changed. Samples were saved, but no address was applied.',
84
84
  recommended: 'Recommended',
85
85
  systemDns: 'System DNS',
86
- resultHint: 'Sorted by robust score. ⚠ flags questionable samples; raw values are retained. Fewer than 2 reliable samples cannot be recommended.',
87
- score: 'Robust score',
88
- scoreHint: 'Q25 / (1 + IQR / median) × reliable sample rate². Higher is better; this is a heuristic, not a confidence interval.',
86
+ resultHint: 'V2 scores rank first; provisional scores need at least 2 reliable samples for recommendation. ⚠ retains raw TPS: high values may be capped, not failed. Skipped nodes are eliminated; legacy records need a retest.',
87
+ score: 'Composite score',
88
+ scoreHint: '100 × (80% capped harmonic TPS + 20% capped Q25) / median node harmonic TPS × actual success rate² × measurement confidence. Confidence = reliable/successful requests × min(1, reliable runs/2). Unstarted, skipped and cancelled requests are not failures.',
89
+ provisional: 'Provisional',
90
+ confirmed: 'Confirmed',
91
+ eliminated: 'Eliminated / skipped',
92
+ legacyRecord: 'Legacy record; retest required',
93
+ incompleteRetest: 'Incomplete retest; this result is not confirmed.',
94
+ appliedIncompleteHint: 'Address applied manually before the full retest completed; this result is not confirmed.',
95
+ appliedLegacyHint: 'Address applied manually from a legacy record; retest with scoring v2 before relying on it.',
96
+ noRecommendationHint: 'Test complete. Not enough reliable evidence for a recommendation; current address unchanged.',
97
+ measured: 'tested',
98
+ skipped: 'skipped',
99
+ budget: 'planned',
100
+ round: 'Round',
101
+ leading: 'Provisional leader',
102
+ successfulSamples: 'successful',
103
+ failedSamples: 'failed',
104
+ measurementConfidence: 'Measurement confidence',
105
+ scoreUncalibrated: 'Provisional calibration',
106
+ scoreReference: 'Shared speed reference',
107
+ scoreCap: 'Shared speed cap',
108
+ scoreUncapped: 'not capped',
109
+ scoredTps: 'TPS used for scoring',
110
+ timeoutBudget: 'timeout budget',
89
111
  stop: 'Stop test',
90
112
  stopping: 'Stopping…',
91
113
  stopped: 'Stopped',
92
114
  stoppedHint: 'Stopped. Completed samples were saved; the current address was not changed.',
93
115
  stoppingHint: 'Cancelling active requests and saving completed samples…',
94
116
  sampleMissing: 'not tested',
117
+ sampleSkipped: 'skipped',
95
118
  sampleStopped: 'stopped',
96
119
  sampleFailed: 'failed',
97
120
  sampleUnmeasurable: 'unmeasurable',
98
121
  sampleLegacy: 'individual samples unavailable; retest',
99
122
  samplesInsufficient: 'insufficient reliable samples',
100
123
  reliableSamples: 'reliable',
101
- sampleReasons: { 'transport-error': 'request failed', cancelled: 'cancelled', unmeasurable: 'invalid usage/timing', 'insufficient-tokens': 'fewer than 32 output tokens', 'missing-decode-window': 'missing decode duration', 'invalid-decode-window': 'invalid decode duration', 'insufficient-decode-window': 'decode window below 200 ms', 'high-outlier': 'over 4× typical TPS; excluded from scoring', 'low-tail': 'slow sample retained in scoring' },
124
+ sampleReasons: { 'transport-error': 'request failed', cancelled: 'cancelled', unmeasurable: 'invalid usage/timing', 'insufficient-tokens': 'fewer than 32 output tokens', 'missing-decode-window': 'missing decode duration', 'invalid-decode-window': 'invalid decode duration', 'insufficient-decode-window': 'decode window below 200 ms', 'high-outlier': 'legacy high-outlier flag; retest with scoring v2', 'high-capped': 'raw high TPS retained; only its scoring value is capped, not a request failure', 'low-tail': 'slow sample retained in scoring' },
102
125
  forget: 'Clear and restore system DNS',
103
126
  probedAgo: 'measured {age} ago',
104
127
  never: 'never',
@@ -163,7 +186,7 @@ window.__ModuleLoader__.load({
163
186
  autoApplyTitle: '测速成功后应用推荐地址;失败保留当前地址。',
164
187
  speedTest: '自动测速',
165
188
  speedTestHint:
166
- '先发现可用地址,再按 ping 从低到高选取不超过上限的 IP(默认全部)并发测速;入选 IP 同样测量(默认 3 次),综合保守速度、波动和有效样本率评分。',
189
+ '先发现地址,优先为上次已选 IP 保留一个测速名额,其余按 ping 从低到高选取(默认全部),按轮次测速(默认 3 轮);综合封顶调和速度、低四分位速度、实际成功率和测量可信度评分,跳过的请求不算失败。',
167
190
  maxBenchmarkIps: '最大测速 IP 数',
168
191
  benchmarkIpLimitAll: '全部',
169
192
  benchmarkIpLimitCustom: '任意(自定义)',
@@ -174,7 +197,7 @@ window.__ModuleLoader__.load({
174
197
  benchmarkCostLimitSummary: '{count} 个最多约{compactTotal} token',
175
198
  benchmarkCostDetails: 'Token 预算详情',
176
199
  benchmarkCostHint: '按 {count} 个有效 IP × 每 IP {runs} 次 × 每次最多 {tokens} 输出 token 估算:约 {total} 输出 token 预算,另计输入 token。实际消耗取决于有效 IP 数和输出长度。',
177
- benchmarkCostAllHint: '“全部”以额外扫描上限 {count} 个 IP 为例;DNS 返回的地址不计入扫描上限,实际 IP 数可能更多。',
200
+ benchmarkCostAllHint: '“全部”以额外扫描上限 {count} 个 IP 为例;DNS 返回地址和上次已选 IP 不计入扫描上限,实际 IP 数可能更多。',
178
201
  stepProbe: '正在发现可用地址',
179
202
  stepBenchmark: '正在测速',
180
203
  stepConfirm: '正在多轮复测',
@@ -215,22 +238,45 @@ window.__ModuleLoader__.load({
215
238
  defaultChangedHint: 'DSH 默认供应商或模型已改变,已保留测试样本,但未应用地址。',
216
239
  recommended: '推荐地址',
217
240
  systemDns: '系统 DNS',
218
- resultHint: '按稳健得分降序排列。⚠ 表示存疑样本,原值保留;少于 2 次可靠样本不会被推荐。',
219
- score: '稳健得分',
220
- scoreHint: '得分 = Q25 ÷ (1 + IQR/中位数) × 有效样本率²。越高越好;这是保守策略,不是统计置信区间。',
241
+ resultHint: '优先按 v2 综合分排名;暂列分需至少 2 次可信样本才可推荐。⚠ 保留原始 TPS,高值仅封顶而非失败;跳过节点视为淘汰,旧记录需重测。',
242
+ score: '综合得分',
243
+ scoreHint: '得分 = 100 ×(80% 封顶调和 TPS + 20% 封顶 Q25)÷ 各节点调和 TPS 中位数 × 实际成功率² × 测量可信度。可信度 = 可信样本/成功请求 × min(1, 可信样本数/2)。未测、跳过、取消均不是失败。',
244
+ provisional: '暂列',
245
+ confirmed: '已确认',
246
+ eliminated: '淘汰 / 已跳过',
247
+ legacyRecord: '旧记录,需重测',
248
+ incompleteRetest: '未完整复测,此结果尚未确认。',
249
+ appliedIncompleteHint: '已手动应用尚未完整复测的地址;此结果尚未确认。',
250
+ appliedLegacyHint: '已手动应用旧记录地址,请使用 v2 评分重新测速后再作判断。',
251
+ noRecommendationHint: '测试完成,可信证据不足以推荐;当前地址未改变。',
252
+ measured: '已测',
253
+ skipped: '跳过',
254
+ budget: '计划',
255
+ round: '轮次',
256
+ leading: '可信暂列',
257
+ successfulSamples: '成功',
258
+ failedSamples: '失败',
259
+ measurementConfidence: '测量可信度',
260
+ scoreUncalibrated: '暂未完成校准',
261
+ scoreReference: '全场共同速度基准',
262
+ scoreCap: '全场共同速度上限',
263
+ scoreUncapped: '未封顶',
264
+ scoredTps: '评分使用 TPS',
265
+ timeoutBudget: '超时预算',
221
266
  stop: '停止测试',
222
267
  stopping: '正在停止…',
223
268
  stopped: '已停止',
224
269
  stoppedHint: '已停止,已完成的样本已保留;当前地址未改变。',
225
270
  stoppingHint: '正在取消在途请求,并保存已完成的样本…',
226
271
  sampleMissing: '未测',
272
+ sampleSkipped: '跳过',
227
273
  sampleStopped: '停止',
228
274
  sampleFailed: '失败',
229
275
  sampleUnmeasurable: '不可测',
230
276
  sampleLegacy: '逐次值未记录,需重新测速',
231
277
  samplesInsufficient: '可靠样本不足',
232
278
  reliableSamples: '可靠',
233
- sampleReasons: { 'transport-error': '请求失败', cancelled: '已取消', unmeasurable: '用量或计时无效', 'insufficient-tokens': '输出不足 32 token', 'missing-decode-window': '缺少出词时长', 'invalid-decode-window': '出词时长无效', 'insufficient-decode-window': '出词计时不足 200 毫秒', 'high-outlier': '超过典型 TPS 的 4 倍,不参与评分', 'low-tail': '低速样本,仍计入波动和评分' },
279
+ sampleReasons: { 'transport-error': '请求失败', cancelled: '已取消', unmeasurable: '用量或计时无效', 'insufficient-tokens': '输出不足 32 token', 'missing-decode-window': '缺少出词时长', 'invalid-decode-window': '出词时长无效', 'insufficient-decode-window': '出词计时不足 200 毫秒', 'high-outlier': '旧版高值异常标记,请使用 v2 重测', 'high-capped': '原始高 TPS 保留,仅评分值封顶,不是请求失败', 'low-tail': '低速样本,仍计入评分' },
234
280
  forget: '清除并恢复系统 DNS',
235
281
  probedAgo: '{age}前测量',
236
282
  never: '从未',
@@ -363,6 +409,9 @@ window.__ModuleLoader__.load({
363
409
  chips: { display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' },
364
410
  mono: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 },
365
411
  table: { width: '100%', minWidth: 740, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, fontSize: 12 },
412
+ runsTable: { width: '100%', minWidth: 0, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, fontSize: 12 },
413
+ runsHead: { display: 'block', fontSize: 10, fontWeight: 400, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.35 },
414
+ runsCell: { padding: '7px 4px', borderBottom: '1px solid var(--dsw-alias-border-l1)', verticalAlign: 'top', minWidth: 0, fontVariantNumeric: 'tabular-nums' },
366
415
  resultsSurface: { width: '100%', minWidth: 0, containerType: 'inline-size', containerName: 'dsh-best-ip-results' },
367
416
  resultViewport: { width: '100%', minWidth: 0, maxHeight: 'min(60vh, 560px)', overflow: 'auto', position: 'relative', isolation: 'isolate', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 9, boxSizing: 'border-box' },
368
417
  resultIdentity: { position: 'sticky', left: 0, zIndex: 2, background: 'var(--dsw-alias-bg-layer-1)', borderRight: '1px solid var(--dsw-alias-border-l1)' },
@@ -510,6 +559,10 @@ window.__ModuleLoader__.load({
510
559
  /** Human-readable, specific progress line from the server's live snapshot. */
511
560
  function describeProgress(progress, t) {
512
561
  if (progress === null || progress === undefined) return t('stepProbe')
562
+ const budget = `${progress.timing ?? 0} ${t('measured')} + ${progress.skipped ?? 0} ${t('skipped')} / ${progress.timingTotal ?? 0} ${t('budget')}`
563
+ const round = progress.round > 0 && progress.rounds > 0 ? ` · ${t('round')} ${progress.round}/${progress.rounds}` : ''
564
+ const leading = progress.leading ? ` · ${t('leading')}: ${progress.leading}` : ''
565
+ const timing = ` · ${budget}${round}`
513
566
  switch (progress.stage) {
514
567
  case 'discover':
515
568
  return `${t('stepProbe')} · ${t('resolversDone')} ${progress.resolversDone ?? 0}/${progress.resolvers ?? 0}`
@@ -517,15 +570,15 @@ window.__ModuleLoader__.load({
517
570
  case 'scan':
518
571
  return `${t('stepProbe')} · ${progress.attempted ?? 0}/${progress.total ?? 0} ${t('probed')} · ${progress.usable ?? 0} ${t('usable')}`
519
572
  case 'timing':
520
- return `${t('stepBenchmark')} · ${progress.timing ?? 0}/${progress.timingTotal ?? 0}`
573
+ return `${t('stepBenchmark')}${timing}${leading}`
521
574
  case 'confirm':
522
- return `${t('stepConfirm')} · ${(progress.rows ?? []).length} ${t('addresses')} · ${progress.timing ?? 0}/${progress.timingTotal ?? 0}`
575
+ return `${t('stepConfirm')} · ${(progress.rows ?? []).length} ${t('addresses')}${timing}${leading}`
523
576
  case 'stopping':
524
- return t('stopping')
577
+ return `${t('stopping')}${progress.timingTotal !== undefined ? timing : ''}`
525
578
  case 'stopped':
526
- return t('stopped')
579
+ return `${t('stopped')}${progress.timingTotal !== undefined ? timing : ''}`
527
580
  case 'done':
528
- return t('stepDone')
581
+ return `${t('stepDone')}${progress.timingTotal !== undefined ? timing : ''}`
529
582
  case 'failed':
530
583
  return progress.message ?? t('stepFailed')
531
584
  default:
@@ -541,46 +594,47 @@ window.__ModuleLoader__.load({
541
594
  return [failure, outcome?.saveError].filter(Boolean).join('\n') || null
542
595
  }
543
596
 
544
- /**
545
- * Addresses as rows, their runs as columns.
546
- *
547
- * A sweep produces one run per address, the confirm phase produces several
548
- * per address; laying runs out horizontally keeps both readable and makes
549
- * "same address, second attempt" obvious at a glance.
550
- *
551
- * Only TPS is shown here. The choice this table supports is which address
552
- * streams fastest, and TTFT next to it crowded the row without changing any
553
- * decision — the measured column is the one worth the width.
554
- */
597
+ /** Rows arrive in the server's round-end rank order; never reshuffle them mid-round. */
555
598
  function RunsTable({ progress, t }) {
556
599
  const rows = Array.isArray(progress?.rows) ? progress.rows : []
557
600
  if (rows.length === 0) return null
558
- const width = Math.max(1, ...rows.map((row) => (Array.isArray(row.runs) ? row.runs.length : 0)))
559
- const columns = []
560
- for (let index = 0; index < width; index++) columns.push(index)
561
- return h(
562
- 'div',
563
- { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
564
- h('div', { style: { ...S.mono, ...S.muted, display: 'flex', gap: 12 } },
565
- h('span', { style: { minWidth: 132 } }, ''),
566
- columns.map((index) => h('span', { key: index, style: { minWidth: 118 } }, `${t('run')}${index + 1}`)),
567
- ),
568
- rows.map((row) =>
569
- h('div', { key: row.ip, style: { ...S.mono, display: 'flex', gap: 12, alignItems: 'baseline' } },
570
- h('span', { style: { minWidth: 132 } }, `${row.ip}${row.colo ? ` ${row.colo}` : ''}`),
571
- columns.map((index) => {
572
- const run = Array.isArray(row.runs) ? row.runs[index] : undefined
573
- return h(
574
- 'span',
575
- { key: index, style: { minWidth: 118, ...S.muted } },
576
- run === undefined
577
- ? '…'
578
- : run.error !== undefined && run.error !== null
579
- ? h('span', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, `✗ ${String(run.error).slice(0, 40)}`)
580
- : `${run.tps === null || run.tps === undefined ? '-' : run.tps} t/s`,
581
- )
582
- }),
601
+ const width = Math.max(1, progress.rounds ?? 0, ...rows.map((row) => Math.max(row.summary?.plannedRuns ?? 0, Array.isArray(row.runs) ? row.runs.length : 0)))
602
+ const columns = Array.from({ length: width }, (_, index) => index)
603
+ // Fixed columns squeezed the third round off the panel. Share the remaining
604
+ // width between rounds so the default three fit without sideways scrolling.
605
+ return h('div', { style: { width: '100%', minWidth: 0, overflowX: 'auto' } },
606
+ h('table', { style: S.runsTable },
607
+ h('colgroup', null,
608
+ h('col', { style: { width: '38%' } }),
609
+ h('col', { style: { width: '22%' } }),
610
+ columns.map((index) => h('col', { key: index, style: { width: `${40 / width}%` } })),
583
611
  ),
612
+ h('thead', null, h('tr', null,
613
+ h('th', { scope: 'col', style: { ...S.th, padding: '6px 4px' } }, t('ipAndEdge')),
614
+ h('th', { scope: 'col', style: { ...S.th, padding: '6px 4px' }, title: t('scoreHint') }, t('score')),
615
+ columns.map((index) => h('th', { key: index, scope: 'col', style: { ...S.th, padding: '6px 4px', textAlign: 'right' } },
616
+ h('span', { style: { fontWeight: 500 } }, `${t('run')}${index + 1}`),
617
+ )),
618
+ )),
619
+ h('tbody', null, rows.map((row, index) => {
620
+ // Old live snapshots can still display real attempts, but never gain a v2 score.
621
+ const summary = { ...row.summary, samples: Array.isArray(row.summary?.samples) ? row.summary.samples : row.runs ?? [], plannedRuns: Math.max(width, row.summary?.plannedRuns ?? 0) }
622
+ const values = sampleValues(summary, t).slice(1, -1).split(', ')
623
+ const details = sampleDetails(summary, t).split('\n')
624
+ return h('tr', { key: row.ip },
625
+ h('td', { style: { ...S.runsCell, ...S.mono, overflowWrap: 'anywhere' } },
626
+ h('div', null, `${row.rank ?? index + 1}. ${row.ip}`),
627
+ row.colo ? h('div', { style: { ...S.muted, fontSize: 10 } }, row.colo) : null,
628
+ ),
629
+ h('td', { style: S.runsCell }, h(ResultScore, { bench: row.summary, t })),
630
+ columns.map((column) => h('td', {
631
+ key: column,
632
+ title: details[column],
633
+ style: { ...S.runsCell, ...S.mono, ...S.muted, padding: column === width - 1 ? '7px 0 7px 4px' : '7px 4px',
634
+ textAlign: 'right', whiteSpace: 'normal', overflowWrap: 'anywhere' },
635
+ }, values[column] ?? t('sampleMissing'))),
636
+ )
637
+ })),
584
638
  ),
585
639
  )
586
640
  }
@@ -622,23 +676,48 @@ window.__ModuleLoader__.load({
622
676
  function orderedCandidates(candidates, benchmarks) {
623
677
  const group = (entry) => {
624
678
  const bench = benchmarks?.[entry.ip]
625
- if (bench?.rankEligible && bench.scoringVersion === 1 && Number.isFinite(bench.stabilityScore)) return 0
626
- if (Array.isArray(bench?.samples) && bench.samples.length > 0) return 1
627
- if (Number.isFinite(bench?.tps)) return 2
628
- return 3
679
+ const sampled = Array.isArray(bench?.samples) && bench.samples.length > 0
680
+ if (bench?.scoringVersion === 2 && !bench.historical) {
681
+ if (bench.eliminated) return 2
682
+ if (Number.isFinite(bench.stabilityScore) && bench.stabilityScore > 0) return 0
683
+ return sampled ? 1 : 4
684
+ }
685
+ return sampled || Number.isFinite(bench?.tps) ? 3 : 4
629
686
  }
687
+ const value = (candidate, key, fallback = 0) => {
688
+ const bench = benchmarks?.[candidate.ip]
689
+ return bench?.scoringVersion === 2 && !bench.historical && Number.isFinite(bench[key]) ? bench[key] : fallback
690
+ }
691
+ const rtt = (candidate) => Number.isFinite(candidate.rtt) ? candidate.rtt : Infinity
692
+ // Keep this tuple identical to the server. Old scores, including another v2 cohort, are incomparable.
630
693
  return [...(candidates ?? [])].sort((a, b) => group(a) - group(b) ||
631
- ((benchmarks?.[b.ip]?.stabilityScore ?? -1) - (benchmarks?.[a.ip]?.stabilityScore ?? -1)) ||
632
- (a.rtt ?? Infinity) - (b.rtt ?? Infinity) || String(a.ip).localeCompare(String(b.ip)))
694
+ value(b, 'stabilityScore') - value(a, 'stabilityScore') ||
695
+ value(b, 'reliableRuns') - value(a, 'reliableRuns') ||
696
+ value(b, 'effectiveTps') - value(a, 'effectiveTps') ||
697
+ value(b, 'conservativeTps') - value(a, 'conservativeTps') ||
698
+ value(a, 'ttftMs', Infinity) - value(b, 'ttftMs', Infinity) ||
699
+ rtt(a) - rtt(b) || String(a.ip).localeCompare(String(b.ip)))
700
+ }
701
+
702
+ // Skipped slots are display-only: samples continue to mean real attempts in their original order.
703
+ function sampleSlots(bench) {
704
+ if (!Array.isArray(bench?.samples)) return null
705
+ const skipped = Number.isSafeInteger(bench.skippedRuns) && bench.skippedRuns > 0 ? bench.skippedRuns : 0
706
+ const pending = Number.isSafeInteger(bench.pendingRuns) && bench.pendingRuns > 0 ? bench.pendingRuns : 0
707
+ const planned = Number.isSafeInteger(bench.plannedRuns) && bench.plannedRuns > 0 ? bench.plannedRuns : 0
708
+ const count = Math.max(bench.samples.length + skipped + pending, planned)
709
+ return Array.from({ length: count }, (_, index) => ({
710
+ sample: bench.samples[index],
711
+ missing: index >= bench.samples.length && index < bench.samples.length + skipped ? 'sampleSkipped' : 'sampleMissing',
712
+ }))
633
713
  }
634
714
 
635
715
  function sampleValues(bench, t) {
636
- if (!Array.isArray(bench?.samples)) return t('sampleLegacy')
637
- const count = Math.max(bench.samples.length, bench.plannedRuns ?? 0)
638
- const values = Array.from({ length: count }, (_, index) => {
639
- const sample = bench.samples[index]
640
- if (!sample) return t('sampleMissing')
641
- if (sample.cancelled) return t('sampleStopped')
716
+ const slots = sampleSlots(bench)
717
+ if (slots === null) return t('sampleLegacy')
718
+ const values = slots.map(({ sample, missing }) => {
719
+ if (!sample) return t(missing)
720
+ if (sample.cancelled || sample.error === 'test stopped') return t('sampleStopped')
642
721
  if (Number.isFinite(sample.tps)) return `${sample.tps.toFixed(1)}${sample.flags?.length ? '⚠' : ''}`
643
722
  return sample.error ? t('sampleFailed') : t('sampleUnmeasurable')
644
723
  })
@@ -646,15 +725,22 @@ window.__ModuleLoader__.load({
646
725
  }
647
726
 
648
727
  function sampleDetails(bench, t) {
649
- if (!Array.isArray(bench?.samples)) return t('sampleLegacy')
650
- return bench.samples.map((sample, index) => {
728
+ const slots = sampleSlots(bench)
729
+ if (slots === null) return t('sampleLegacy')
730
+ return slots.map(({ sample, missing }, index) => {
731
+ const label = `${t('run')}${index + 1}: `
732
+ if (!sample) return `${label}${t(missing)}${missing === 'sampleSkipped' && bench.skippedReason ? `; ${bench.skippedReason}` : ''}`
651
733
  const flags = (sample.flags ?? []).map((flag) => t(`sampleReasons.${flag}`)).join('; ')
652
- return `${t('run')}${index + 1}: ${sample.tps ?? '-'} TPS; ${sample.outputTokens ?? '-'} tokens; ${sample.decodeMs ?? '-'} ms${flags ? `; ${flags}` : ''}`
734
+ const state = sample.cancelled || sample.error === 'test stopped' ? `${t('sampleStopped')}; ` : sample.error ? `${t('sampleFailed')}; ` : ''
735
+ const scored = Number.isFinite(sample.scoredTps) ? `; ${t('scoredTps')}: ${sample.scoredTps}` : ''
736
+ const timeout = Number.isFinite(sample.timeoutMs) && sample.timeoutMs > 0 ? `; ${t('timeoutBudget')}: ${sample.timeoutMs} ms` : ''
737
+ return `${label}${state}${sample.tps ?? '-'} TPS; ${sample.outputTokens ?? '-'} tokens; ${sample.decodeMs ?? '-'} ms${scored}${timeout}${flags ? `; ${flags}` : ''}`
653
738
  }).join('\n')
654
739
  }
655
740
 
656
741
  function SampleBadges({ bench, t }) {
657
742
  if (!Array.isArray(bench?.samples)) return h('div', { style: { ...S.muted, whiteSpace: 'normal' } }, t('sampleLegacy'))
743
+ if (sampleSlots(bench).length === 0) return null
658
744
  const labels = sampleValues(bench, t).slice(1, -1).split(', ')
659
745
  const details = sampleDetails(bench, t).split('\n')
660
746
  return h('div', { style: S.sampleBadges, 'aria-label': t('samplesLabel') },
@@ -676,12 +762,14 @@ window.__ModuleLoader__.load({
676
762
  )
677
763
  }
678
764
 
679
- function ResultApply({ ip, selected, disabled, onChoose, label, t }) {
765
+ function ResultApply({ ip, selected, disabled, onChoose, label, bench, t }) {
680
766
  if (selected) return h('span', { style: { ...S.muted, display: 'block', textAlign: 'center', padding: '7px 0' } }, `✓ ${t('inUse')}`)
767
+ const warning = bench?.scoringVersion === 2 && !bench.historical ? bench.confirmed !== true ? t('incompleteRetest') : ''
768
+ : bench && ((bench.samples?.length ?? 0) > 0 || Number.isFinite(bench.tps) || Number.isFinite(bench.stabilityScore)) ? t('legacyRecord') : ''
681
769
  return h('button', {
682
770
  type: 'button', className: 'dsh-best-ip-result-apply',
683
771
  style: { ...S.btn, ...S.resultApply }, disabled,
684
- title: `${label ?? t('use')}: ${ip}`, 'aria-label': `${label ?? t('use')}: ${ip}`,
772
+ title: `${label ?? t('use')}: ${ip}${warning ? ` · ${warning}` : ''}`, 'aria-label': `${label ?? t('use')}: ${ip}`,
685
773
  onClick: () => onChoose(ip),
686
774
  }, label ?? t('use'))
687
775
  }
@@ -695,19 +783,39 @@ window.__ModuleLoader__.load({
695
783
  }
696
784
 
697
785
  function ResultScore({ bench, t }) {
698
- return h('div', { style: S.resultMetric, title: t('scoreHint') },
699
- h('div', { style: { fontWeight: 600, fontVariantNumeric: 'tabular-nums' } }, bench?.rankEligible && Number.isFinite(bench.stabilityScore) ? bench.stabilityScore.toFixed(1) : '—'),
700
- bench?.scoringVersion === 1 ? h('div', { style: { ...S.muted, fontSize: 11 } }, `${bench.reliableRuns ?? 0}/${bench.plannedRuns ?? bench.runs ?? 0} ${t('reliableSamples')}`) : null,
701
- (bench?.samples?.length ?? 0) > 0 && bench?.rankEligible === false ? h('div', { style: { ...S.muted, fontSize: 11 } }, t('samplesInsufficient')) : null,
786
+ const samples = Array.isArray(bench?.samples) ? bench.samples : []
787
+ const measured = samples.length > 0 || (bench?.runs ?? 0) > 0 || Number.isFinite(bench?.tps) || Number.isFinite(bench?.stabilityScore)
788
+ const v2 = bench?.scoringVersion === 2 && !bench.historical
789
+ const eliminated = v2 && (bench.eliminated === true || bench.skippedRuns > 0)
790
+ const status = !v2 ? measured ? 'legacyRecord' : 'untested'
791
+ : eliminated ? 'eliminated' : bench.confirmed === true && bench.rankEligible === true ? 'confirmed' : measured ? 'provisional' : 'untested'
792
+ const successful = bench?.successfulRuns ?? (samples.length > 0
793
+ ? samples.filter((sample) => sample && !sample.cancelled && !sample.error).length
794
+ : Math.max(0, (bench?.runs ?? 0) - (bench?.failed ?? 0) - (bench?.cancelled ?? 0)))
795
+ const failed = bench?.failed ?? samples.filter((sample) => sample && !sample.cancelled && sample.error && sample.error !== 'test stopped').length
796
+ const confidence = Number.isFinite(bench?.measurementConfidence) ? `\n${t('measurementConfidence')}: ${(bench.measurementConfidence * 100).toFixed(0)}%` : ''
797
+ const context = v2 ? bench.scoreContext : undefined
798
+ const shared = context ? `\n${t('scoreReference')}: ${context.speedReference} TPS\n${t('scoreCap')}: ${Number.isFinite(context.speedCap) ? `${context.speedCap} TPS` : t('scoreUncapped')}` : ''
799
+ const calibration = context?.calibrated === false ? `\n${t('scoreUncalibrated')}` : ''
800
+ const counts = `${successful} ${t('successfulSamples')} · ${failed} ${t('failedSamples')} · ${bench?.reliableRuns ?? 0} ${t('reliableSamples')}`
801
+ return h('div', { style: S.resultMetric, title: `${v2 ? t('scoreHint') : t('legacyRecord')}${shared}${confidence}${calibration}` },
802
+ // A single trustworthy run has a useful provisional score even though it cannot be recommended yet.
803
+ h('div', { style: { fontWeight: 600, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } }, Number.isFinite(bench?.stabilityScore) ? bench.stabilityScore.toFixed(1) : '—'),
804
+ h('div', { style: { ...S.muted, fontSize: 11 } }, t(status)),
805
+ measured || eliminated ? h('div', { style: { ...S.muted, fontSize: 11 } }, counts) : null,
806
+ v2 && measured && (bench.reliableRuns ?? 0) < 2 ? h('div', { style: { ...S.muted, fontSize: 11 } }, t('samplesInsufficient')) : null,
702
807
  )
703
808
  }
704
809
 
705
810
  function ResultTiming({ row, bench, t }) {
706
- const attempted = Array.isArray(bench?.samples) ? bench.samples.length : bench?.runs ?? 0
707
- const failed = attempted > 0 && (bench.error !== undefined || bench.eligible === false)
811
+ const samples = Array.isArray(bench?.samples) ? bench.samples : []
812
+ const attempted = samples.length || bench?.runs || 0
813
+ const cancelled = (bench?.cancelled ?? 0) > 0 || samples.some((sample) => sample?.cancelled || sample?.error === 'test stopped')
814
+ const failed = attempted > 0 && ((bench?.failed ?? 0) > 0 || samples.some((sample) => sample?.error && !sample.cancelled && sample.error !== 'test stopped') ||
815
+ (!cancelled && (bench?.error !== undefined || bench?.eligible === false)))
708
816
  return h('div', { style: S.resultMetric },
709
817
  h('div', { style: S.muted }, `${t('ping')}: ${Number.isFinite(row.rtt) ? `${row.rtt} ms` : '—'}`),
710
- h('div', { style: S.muted }, `${t('ttft')}: ${Number.isFinite(bench?.ttftMs) ? `${bench.ttftMs} ms` : failed ? t('unusable') : t('untested')}`),
818
+ h('div', { style: S.muted }, `${t('ttft')}: ${Number.isFinite(bench?.ttftMs) ? `${bench.ttftMs} ms` : failed ? t('unusable') : cancelled ? t('sampleStopped') : t('untested')}`),
711
819
  )
712
820
  }
713
821
 
@@ -866,7 +974,7 @@ window.__ModuleLoader__.load({
866
974
  if (runError) setError(runError)
867
975
  if (final?.stage === 'stopped') setNote(t('stoppedHint'))
868
976
  if (final?.stage === 'done') {
869
- setNote(final.defaultChanged ? t('defaultChangedHint') : final.applied ? t('applies') : t('previewDone').replace('{ip}', final.recommended ?? '-'))
977
+ setNote(final.defaultChanged ? t('defaultChangedHint') : final.applied ? t('applies') : final.recommended ? t('previewDone').replace('{ip}', final.recommended) : t('noRecommendationHint'))
870
978
  }
871
979
  } catch (problem) {
872
980
  setError(problem?.message ?? String(problem))
@@ -956,9 +1064,11 @@ window.__ModuleLoader__.load({
956
1064
  setError(null)
957
1065
  try {
958
1066
  const result = await call('/select', { host: measurement.hostname, provider: selection.provider, ip, testOnly: false })
959
- if (result.result?.saveError) setError(result.result.saveError)
960
1067
  await load()
961
- setNote(null)
1068
+ if (result.result?.saveError) setError(result.result.saveError)
1069
+ const bench = measurement.benchmarks?.[ip]
1070
+ setNote(bench?.scoringVersion === 2 && !bench.historical ? bench.confirmed !== true ? t('appliedIncompleteHint') : null
1071
+ : bench && ((bench.samples?.length ?? 0) > 0 || Number.isFinite(bench.tps) || Number.isFinite(bench.stabilityScore)) ? t('appliedLegacyHint') : null)
962
1072
  } catch (problem) {
963
1073
  setError(problem?.message ?? String(problem))
964
1074
  }
@@ -985,7 +1095,9 @@ window.__ModuleLoader__.load({
985
1095
  const benchmarkCost = benchmarkIpLimitInvalid ? null : describeBenchmarkCost(state, draft.maxBenchmarkIps, t)
986
1096
  const benchmarked = measurement?.benchmarks ?? {}
987
1097
  const sortedRows = orderedCandidates(measurement?.candidates, benchmarked)
988
- const bestIp = sortedRows.find((row) => benchmarked[row.ip]?.rankEligible === true)?.ip
1098
+ const recommendedIp = sortedRows.some((row) => row.ip === measurement?.recommended && benchmarked[row.ip]?.scoringVersion === 2 && !benchmarked[row.ip]?.historical &&
1099
+ benchmarked[row.ip]?.rankEligible === true && !benchmarked[row.ip]?.eliminated && Number.isFinite(benchmarked[row.ip]?.stabilityScore) && benchmarked[row.ip].stabilityScore > 0)
1100
+ ? measurement.recommended : undefined
989
1101
 
990
1102
  const statItems = [
991
1103
  ['statPinned', state.pinned],
@@ -1187,9 +1299,9 @@ window.__ModuleLoader__.load({
1187
1299
  h('div', { style: { ...S.chips, minWidth: 0 } },
1188
1300
  h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, measurement.hostname),
1189
1301
  h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, `${t('inUse')}: ${measurement.activeAddress ?? t('systemDns')}`),
1190
- measurement.recommended ? h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, `${t('recommended')}: ${measurement.recommended}`) : null,
1191
- measurement.recommended && measurement.recommended !== measurement.activeAddress && sortedRows.some((row) => row.ip === measurement.recommended)
1192
- ? h('div', { style: { width: 148, maxWidth: '100%' } }, h(ResultApply, { ip: measurement.recommended, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, label: t('applyRecommended'), t }))
1302
+ recommendedIp ? h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, `${t('recommended')}: ${recommendedIp}`) : null,
1303
+ recommendedIp && recommendedIp !== measurement.activeAddress
1304
+ ? h('div', { style: { width: 148, maxWidth: '100%' } }, h(ResultApply, { ip: recommendedIp, bench: benchmarked[recommendedIp], disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, label: t('applyRecommended'), t }))
1193
1305
  : null,
1194
1306
  ),
1195
1307
  // Persisted empty results need a message even after reopening the page.
@@ -1211,13 +1323,13 @@ window.__ModuleLoader__.load({
1211
1323
  h('tbody', null, sortedRows.map((row) => {
1212
1324
  const bench = benchmarked[row.ip]
1213
1325
  const selected = measurement.activeAddress === row.ip
1214
- const recommended = bench?.rankEligible === true && row.ip === bestIp
1326
+ const recommended = row.ip === recommendedIp
1215
1327
  return h('tr', { key: row.ip },
1216
1328
  h('td', { style: { ...S.td, ...S.resultIdentity } }, h(ResultIdentity, { row, selected, recommended, t })),
1217
1329
  h('td', { style: S.td }, h(ResultSpeed, { bench, t })),
1218
1330
  h('td', { style: { ...S.td, ...(recommended ? S.best : {}) } }, h(ResultScore, { bench, t })),
1219
1331
  h('td', { style: S.td }, h(ResultTiming, { row, bench, t })),
1220
- h('td', { style: { ...S.td, ...S.resultAction } }, h(ResultApply, { ip: row.ip, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
1332
+ h('td', { style: { ...S.td, ...S.resultAction } }, h(ResultApply, { ip: row.ip, bench, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
1221
1333
  )
1222
1334
  })),
1223
1335
  ),
@@ -1225,11 +1337,11 @@ window.__ModuleLoader__.load({
1225
1337
  h('div', { className: 'dsh-best-ip-results-cards', style: S.resultCards }, sortedRows.map((row) => {
1226
1338
  const bench = benchmarked[row.ip]
1227
1339
  const selected = measurement.activeAddress === row.ip
1228
- const recommended = bench?.rankEligible === true && row.ip === bestIp
1340
+ const recommended = row.ip === recommendedIp
1229
1341
  return h('article', { key: row.ip, style: S.resultCard, 'aria-label': row.ip },
1230
1342
  h('div', { style: S.resultCardHeader },
1231
1343
  h('div', { style: { flex: '1 1 auto', minWidth: 0 } }, h(ResultIdentity, { row, selected, recommended, t })),
1232
- h('div', { style: { flex: '0 0 112px', maxWidth: '48%' } }, h(ResultApply, { ip: row.ip, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
1344
+ h('div', { style: { flex: '0 0 112px', maxWidth: '48%' } }, h(ResultApply, { ip: row.ip, bench, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
1233
1345
  ),
1234
1346
  h('div', { style: S.resultCardMetrics },
1235
1347
  h('div', { style: S.resultMetric }, h('div', { style: S.hint }, t('medianTps')), h(ResultSpeed, { bench, t })),