free-coding-models 0.1.84 β†’ 0.1.86

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.
@@ -43,29 +43,29 @@ import { calculateViewport, sortResultsWithPinnedFavorites, renderProxyStatusLin
43
43
  const require = createRequire(import.meta.url)
44
44
  const { version: LOCAL_VERSION } = require('../package.json')
45
45
 
46
- // πŸ“– Provider column palette: keep all Origins in the same visual family
47
- // πŸ“– (blue/cyan tones) while making each provider easy to distinguish at a glance.
46
+ // πŸ“– Provider column palette: soft pastel rainbow so each provider stays easy
47
+ // πŸ“– to spot without turning the table into a harsh neon wall.
48
48
  const PROVIDER_COLOR = {
49
- nvidia: [120, 205, 255],
50
- groq: [95, 185, 255],
51
- cerebras: [70, 165, 255],
52
- sambanova: [45, 145, 245],
53
- openrouter: [135, 220, 255],
54
- huggingface: [110, 190, 235],
55
- replicate: [85, 175, 230],
56
- deepinfra: [60, 160, 225],
57
- fireworks: [125, 215, 245],
58
- codestral: [100, 180, 240],
59
- hyperbolic: [75, 170, 240],
60
- scaleway: [55, 150, 235],
61
- googleai: [130, 210, 255],
62
- siliconflow: [90, 195, 245],
63
- together: [65, 155, 245],
64
- cloudflare: [115, 200, 240],
65
- perplexity: [140, 225, 255],
66
- qwen: [80, 185, 235],
67
- zai: [50, 140, 225],
68
- iflow: [145, 230, 255],
49
+ nvidia: [178, 235, 190],
50
+ groq: [255, 204, 188],
51
+ cerebras: [179, 229, 252],
52
+ sambanova: [255, 224, 178],
53
+ openrouter: [225, 190, 231],
54
+ huggingface: [255, 245, 157],
55
+ replicate: [187, 222, 251],
56
+ deepinfra: [178, 223, 219],
57
+ fireworks: [255, 205, 210],
58
+ codestral: [248, 187, 208],
59
+ hyperbolic: [200, 230, 201],
60
+ scaleway: [129, 212, 250],
61
+ googleai: [187, 222, 251],
62
+ siliconflow: [178, 235, 242],
63
+ together: [197, 225, 165],
64
+ cloudflare: [255, 204, 128],
65
+ perplexity: [159, 234, 201],
66
+ qwen: [255, 224, 130],
67
+ zai: [174, 213, 255],
68
+ iflow: [220, 231, 117],
69
69
  }
70
70
 
71
71
  // πŸ“– Active proxy reference for footer status line (set by bin/free-coding-models.js).
@@ -77,7 +77,7 @@ export function setActiveProxy(proxyInstance) {
77
77
  }
78
78
 
79
79
  // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
80
- export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null) {
80
+ export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false) {
81
81
  // πŸ“– Filter out hidden models for display
82
82
  const visibleResults = results.filter(r => !r.hidden)
83
83
 
@@ -85,31 +85,48 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
85
85
  const down = visibleResults.filter(r => r.status === 'down').length
86
86
  const timeout = visibleResults.filter(r => r.status === 'timeout').length
87
87
  const pending = visibleResults.filter(r => r.status === 'pending').length
88
+ const totalVisible = visibleResults.length
89
+ const completedPings = Math.max(0, totalVisible - pending)
88
90
 
89
91
  // πŸ“– Calculate seconds until next ping
90
92
  const timeSinceLastPing = Date.now() - lastPingTime
91
93
  const timeUntilNextPing = Math.max(0, pingInterval - timeSinceLastPing)
92
- const secondsUntilNext = Math.ceil(timeUntilNextPing / 1000)
94
+ const secondsUntilNext = timeUntilNextPing / 1000
95
+ const secondsUntilNextLabel = secondsUntilNext.toFixed(1)
93
96
 
94
- const phase = pending > 0
95
- ? chalk.dim(`discovering β€” ${pending} remaining…`)
96
- : pendingPings > 0
97
- ? chalk.dim(`pinging β€” ${pendingPings} in flight…`)
98
- : chalk.dim(`next ping ${secondsUntilNext}s`)
99
-
100
- // πŸ“– Mode badge shown in header so user knows what Enter will do
101
- // πŸ“– Now includes key hint for mode toggle
97
+ const intervalSec = Math.round(pingInterval / 1000)
98
+ const pingModeMeta = {
99
+ speed: { label: 'fast', color: chalk.bold.rgb(255, 210, 80) },
100
+ normal: { label: 'normal', color: chalk.bold.rgb(120, 210, 255) },
101
+ slow: { label: 'slow', color: chalk.bold.rgb(255, 170, 90) },
102
+ forced: { label: 'forced', color: chalk.bold.rgb(255, 120, 120) },
103
+ }
104
+ const activePingMode = pingModeMeta[pingMode] ?? pingModeMeta.normal
105
+ const pingProgressText = `${completedPings}/${totalVisible}`
106
+ const nextCountdownColor = secondsUntilNext > 8
107
+ ? chalk.red.bold
108
+ : secondsUntilNext >= 4
109
+ ? chalk.yellow.bold
110
+ : secondsUntilNext < 1
111
+ ? chalk.greenBright.bold
112
+ : chalk.green.bold
113
+ const pingControlBadge =
114
+ activePingMode.color(' [ ') +
115
+ chalk.yellow.bold('W') +
116
+ activePingMode.color(` Ping Interval : ${intervalSec}s (${activePingMode.label}) - ${pingProgressText} - next : `) +
117
+ nextCountdownColor(`${secondsUntilNextLabel}s`) +
118
+ activePingMode.color(' ]')
119
+
120
+ // πŸ“– Tool badge keeps the active launch target visible in the header, so the
121
+ // πŸ“– footer no longer needs a redundant Enter action or mode toggle reminder.
102
122
  let modeBadge
103
123
  if (mode === 'openclaw') {
104
- modeBadge = chalk.bold.rgb(255, 100, 50)(' [🦞 OpenClaw]')
124
+ modeBadge = chalk.bold.rgb(255, 100, 50)(' [ ') + chalk.yellow.bold('Z') + chalk.bold.rgb(255, 100, 50)(' Tool : OpenClaw ]')
105
125
  } else if (mode === 'opencode-desktop') {
106
- modeBadge = chalk.bold.rgb(0, 200, 255)(' [πŸ–₯ Desktop]')
126
+ modeBadge = chalk.bold.rgb(0, 200, 255)(' [ ') + chalk.yellow.bold('Z') + chalk.bold.rgb(0, 200, 255)(' Tool : OpenCode Desktop ]')
107
127
  } else {
108
- modeBadge = chalk.bold.rgb(0, 200, 255)(' [πŸ’» CLI]')
128
+ modeBadge = chalk.bold.rgb(0, 200, 255)(' [ ') + chalk.yellow.bold('Z') + chalk.bold.rgb(0, 200, 255)(' Tool : OpenCode CLI ]')
109
129
  }
110
-
111
- // πŸ“– Add mode toggle hint
112
- const modeHint = chalk.dim.yellow(' (Z to toggle)')
113
130
 
114
131
  // πŸ“– Tier filter badge shown when filtering is active (shows exact tier name)
115
132
  const TIER_CYCLE_NAMES = [null, 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
@@ -155,16 +172,29 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
155
172
  const W_UPTIME = 6
156
173
  const W_TOKENS = 7
157
174
  const W_USAGE = 7
175
+ const MIN_TABLE_WIDTH = 166
176
+
177
+ if (terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH) {
178
+ const lines = []
179
+ const blankLines = Math.max(0, Math.floor(((terminalRows || 24) - 3) / 2))
180
+ const warning = 'Please maximize your terminal for optimal use. The current terminal width is too small for the full table.'
181
+ const padLeft = Math.max(0, Math.floor((terminalCols - warning.length) / 2))
182
+ for (let i = 0; i < blankLines; i++) lines.push('')
183
+ lines.push(' '.repeat(padLeft) + chalk.red.bold(warning))
184
+ while (terminalRows > 0 && lines.length < terminalRows) lines.push('')
185
+ const EL = '\x1b[K'
186
+ return lines.map(line => line + EL).join('\n')
187
+ }
158
188
 
159
189
  // πŸ“– Sort models using the shared helper
160
190
  const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection)
161
191
 
162
192
  const lines = [
163
- ` ${chalk.greenBright.bold('βœ… FCM')}${modeBadge}${modeHint}${tierBadge}${originBadge}${profileBadge} ` +
193
+ ` ${chalk.greenBright.bold(`βœ… Free-Coding-Models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${profileBadge}${chalk.reset('')} ` +
164
194
  chalk.greenBright(`βœ… ${up}`) + chalk.dim(' up ') +
165
195
  chalk.yellow(`⏳ ${timeout}`) + chalk.dim(' timeout ') +
166
196
  chalk.red(`❌ ${down}`) + chalk.dim(' down ') +
167
- phase,
197
+ '',
168
198
  '',
169
199
  ]
170
200
 
@@ -259,6 +289,16 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
259
289
  chalk.dim('─'.repeat(W_USAGE))
260
290
  )
261
291
 
292
+ if (sorted.length === 0) {
293
+ lines.push('')
294
+ if (hideUnconfiguredModels) {
295
+ lines.push(` ${chalk.redBright.bold('Press P to configure your API key.')}`)
296
+ lines.push(` ${chalk.dim('No configured provider currently exposes visible models in the table.')}`)
297
+ } else {
298
+ lines.push(` ${chalk.yellow.bold('No models match the current filters.')}`)
299
+ }
300
+ }
301
+
262
302
  // πŸ“– Viewport clipping: only render models that fit on screen
263
303
  const vp = calculateViewport(terminalRows, scrollOffset, sorted.length)
264
304
 
@@ -286,6 +326,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
286
326
  const prefixDisplayWidth = 2
287
327
  const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
288
328
  const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
329
+ const modelColor = chalk.rgb(...providerRgb)
289
330
  const sweScore = r.sweScore ?? 'β€”'
290
331
  // πŸ“– SWE% colorized on the same gradient as Tier:
291
332
  // β‰₯70% bright neon green (S+), β‰₯60% green (S), β‰₯50% yellow-green (A+),
@@ -315,22 +356,30 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
315
356
  ? chalk.cyan(ctxRaw.padEnd(W_CTX))
316
357
  : chalk.dim(ctxRaw.padEnd(W_CTX))
317
358
 
359
+ // πŸ“– Keep the row-local spinner small and inline so users can still read the last measured latency.
360
+ const buildLatestPingDisplay = (value) => {
361
+ const spinner = r.isPinging ? ` ${FRAMES[frame % FRAMES.length]}` : ''
362
+ return `${value}${spinner}`.padEnd(W_PING)
363
+ }
364
+
318
365
  // πŸ“– Latest ping - pings are objects: { ms, code }
319
366
  // πŸ“– Show response time for 200 (success) and 401 (no-auth but server is reachable)
320
367
  const latestPing = r.pings.length > 0 ? r.pings[r.pings.length - 1] : null
321
368
  let pingCell
322
369
  if (!latestPing) {
323
- pingCell = chalk.dim('β€”β€”β€”'.padEnd(W_PING))
370
+ const placeholder = r.isPinging ? buildLatestPingDisplay('β€”β€”β€”') : 'β€”β€”β€”'.padEnd(W_PING)
371
+ pingCell = chalk.dim(placeholder)
324
372
  } else if (latestPing.code === '200') {
325
373
  // πŸ“– Success - show response time
326
- const str = String(latestPing.ms).padEnd(W_PING)
374
+ const str = buildLatestPingDisplay(String(latestPing.ms))
327
375
  pingCell = latestPing.ms < 500 ? chalk.greenBright(str) : latestPing.ms < 1500 ? chalk.yellow(str) : chalk.red(str)
328
376
  } else if (latestPing.code === '401') {
329
377
  // πŸ“– 401 = no API key but server IS reachable β€” still show latency in dim
330
- pingCell = chalk.dim(String(latestPing.ms).padEnd(W_PING))
378
+ pingCell = chalk.dim(buildLatestPingDisplay(String(latestPing.ms)))
331
379
  } else {
332
380
  // πŸ“– Error or timeout - show "β€”β€”β€”" (error code is already in Status column)
333
- pingCell = chalk.dim('β€”β€”β€”'.padEnd(W_PING))
381
+ const placeholder = r.isPinging ? buildLatestPingDisplay('β€”β€”β€”') : 'β€”β€”β€”'.padEnd(W_PING)
382
+ pingCell = chalk.dim(placeholder)
334
383
  }
335
384
 
336
385
  // πŸ“– Avg ping (just number, no "ms")
@@ -465,10 +514,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
465
514
  uptimeCell = chalk.red(uptimeStr.padEnd(W_UPTIME))
466
515
  }
467
516
 
468
- // πŸ“– When cursor is on this row, render Model and Provider in bright white for readability
469
- const nameCell = isCursor ? chalk.white.bold(favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)) : name
517
+ // πŸ“– Model text now mirrors the provider hue so provider affinity is visible
518
+ // πŸ“– even before the eye reaches the Provider column.
519
+ const nameCell = isCursor ? modelColor.bold(name) : modelColor(name)
470
520
  const sourceCursorText = providerName.padEnd(W_SOURCE)
471
- const sourceCell = isCursor ? chalk.white.bold(sourceCursorText) : source
521
+ const sourceCell = isCursor ? chalk.rgb(...providerRgb).bold(sourceCursorText) : source
472
522
 
473
523
  // πŸ“– Usage column β€” provider-scoped remaining quota when measurable,
474
524
  // πŸ“– otherwise a green dot to show "usable but not meaningfully quantifiable".
@@ -502,12 +552,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
502
552
  const row = ' ' + num + ' ' + tier + ' ' + sweCell + ' ' + ctxCell + ' ' + nameCell + ' ' + sourceCell + ' ' + pingCell + ' ' + avgCell + ' ' + status + ' ' + speedCell + ' ' + stabCell + ' ' + uptimeCell + ' ' + tokensCell + ' ' + usageCell
503
553
 
504
554
  if (isCursor) {
505
- lines.push(chalk.bgRgb(50, 0, 60)(row))
555
+ lines.push(chalk.bgRgb(155, 55, 135)(row))
506
556
  } else if (r.isRecommended) {
507
557
  // πŸ“– Medium green background for recommended models (distinguishable from favorites)
508
558
  lines.push(chalk.bgRgb(15, 40, 15)(row))
509
559
  } else if (r.isFavorite) {
510
- lines.push(chalk.bgRgb(35, 20, 0)(row))
560
+ lines.push(chalk.bgRgb(88, 64, 10)(row))
511
561
  } else {
512
562
  lines.push(row)
513
563
  }
@@ -523,18 +573,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
523
573
  } else {
524
574
  lines.push('')
525
575
  }
526
- const intervalSec = Math.round(pingInterval / 1000)
527
-
528
- // πŸ“– Footer hints adapt based on active mode
529
- const actionHint = mode === 'openclaw'
530
- ? chalk.rgb(255, 100, 50)('Enter→SetOpenClaw')
531
- : mode === 'opencode-desktop'
532
- ? chalk.rgb(0, 200, 255)('Enter→OpenDesktop')
533
- : chalk.rgb(0, 200, 255)('Enter→OpenCode')
534
- // πŸ“– Line 1: core navigation + sorting shortcuts
535
- lines.push(chalk.dim(` ↑↓ Navigate β€’ `) + actionHint + chalk.dim(` β€’ `) + chalk.yellow('F') + chalk.dim(` Favorite β€’ R/Y/O/M/L/A/S/C/H/V/B/U/`) + chalk.yellow('G') + chalk.dim(` Sort β€’ `) + chalk.yellow('T') + chalk.dim(` Tier β€’ `) + chalk.yellow('D') + chalk.dim(` Provider β€’ W↓/=↑ (${intervalSec}s) β€’ `) + chalk.rgb(255, 100, 50).bold('Z') + chalk.dim(` Mode β€’ `) + chalk.yellow('X') + chalk.dim(` Logs β€’ `) + chalk.yellow('P') + chalk.dim(` Settings β€’ `) + chalk.rgb(0, 255, 80).bold('K') + chalk.dim(` Help`))
576
+ // πŸ“– Footer hints keep only navigation and secondary actions now that the
577
+ // πŸ“– active tool target is already visible in the header badge.
578
+ const hotkey = (keyLabel, text) => chalk.yellow(keyLabel) + chalk.dim(text)
579
+ // πŸ“– Line 1: core navigation + filtering shortcuts
580
+ lines.push(chalk.dim(` ↑↓ Navigate β€’ `) + hotkey('F', ' Toggle Favorite') + chalk.dim(` β€’ `) + hotkey('T', ' Tier') + chalk.dim(` β€’ `) + hotkey('D', ' Provider') + chalk.dim(` β€’ `) + hotkey('E', ' Configured Only') + chalk.dim(` β€’ `) + hotkey('X', ' Token Logs') + chalk.dim(` β€’ `) + hotkey('P', ' Settings') + chalk.dim(` β€’ `) + hotkey('K', ' Help'))
536
581
  // πŸ“– Line 2: profiles, recommend, feature request, bug report, and extended hints β€” gives visibility to less-obvious features
537
- lines.push(chalk.dim(` `) + chalk.rgb(200, 150, 255).bold('⇧P') + chalk.dim(` Cycle profile β€’ `) + chalk.rgb(200, 150, 255).bold('⇧S') + chalk.dim(` Save profile β€’ `) + chalk.rgb(0, 200, 180).bold('Q') + chalk.dim(` Smart Recommend β€’ `) + chalk.rgb(57, 255, 20).bold('J') + chalk.dim(` Request feature β€’ `) + chalk.rgb(255, 87, 51).bold('I') + chalk.dim(` Report bug β€’ `) + chalk.yellow('Esc') + chalk.dim(` Close overlay`))
582
+ lines.push(chalk.dim(` `) + hotkey('⇧P', ' Cycle profile') + chalk.dim(` β€’ `) + hotkey('⇧S', ' Save profile') + chalk.dim(` β€’ `) + hotkey('Q', ' Smart Recommend') + chalk.dim(` β€’ `) + hotkey('J', ' Request feature') + chalk.dim(` β€’ `) + hotkey('I', ' Report bug'))
538
583
  // πŸ“– Proxy status line β€” always rendered with explicit state (starting/running/failed/stopped)
539
584
  lines.push(renderProxyStatusLine(proxyStartupStatus, activeProxyRef))
540
585
  lines.push(
@@ -551,8 +596,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
551
596
  chalk.dim(' β†’ ') +
552
597
  chalk.rgb(200, 150, 255)('https://discord.gg/5MbTnDC3Md') +
553
598
  chalk.dim(' β€’ ') +
554
- chalk.dim(`v${LOCAL_VERSION}`) +
555
- chalk.dim(' β€’ ') +
556
599
  chalk.dim('Ctrl+C Exit')
557
600
  )
558
601
 
package/src/utils.js CHANGED
@@ -74,18 +74,23 @@ export const TIER_LETTER_MAP = {
74
74
 
75
75
  // ─── Core Logic Functions ────────────────────────────────────────────────────
76
76
 
77
- // πŸ“– getAvg: Calculate average latency from ONLY successful pings (HTTP 200).
78
- // πŸ“– Failed pings (timeouts, 429s, 500s) are excluded to avoid skewing the average.
79
- // πŸ“– Returns Infinity when no successful pings exist β€” this sorts "unknown" models to the bottom.
77
+ // πŸ“– measureablePingCodes: HTTP codes that still give us a real round-trip latency sample.
78
+ // πŸ“– 200 = normal success, 401 = no key / bad key but the provider endpoint is reachable.
79
+ const measurablePingCodes = new Set(['200', '401'])
80
+
81
+ // πŸ“– getAvg: Calculate average latency from pings that produced a real latency sample.
82
+ // πŸ“– HTTP 200 and 401 both count because a 401 still proves the endpoint responded in X ms.
83
+ // πŸ“– Timeouts and server failures are excluded to avoid mixing availability with raw latency.
84
+ // πŸ“– Returns Infinity when no measurable pings exist β€” this sorts "unknown" models to the bottom.
80
85
  // πŸ“– The rounding to integer avoids displaying fractional milliseconds in the TUI.
81
86
  //
82
87
  // πŸ“– Example:
83
- // pings = [{ms: 200, code: '200'}, {ms: 0, code: '429'}, {ms: 400, code: '200'}]
84
- // β†’ getAvg returns 300 (only the two 200s count: (200+400)/2)
88
+ // pings = [{ms: 200, code: '200'}, {ms: 320, code: '401'}, {ms: 999, code: '500'}]
89
+ // β†’ getAvg returns 260 (only the measurable pings count: (200+320)/2)
85
90
  export const getAvg = (r) => {
86
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
87
- if (successfulPings.length === 0) return Infinity
88
- return Math.round(successfulPings.reduce((a, b) => a + b.ms, 0) / successfulPings.length)
91
+ const measurablePings = (r.pings || []).filter(p => measurablePingCodes.has(p.code))
92
+ if (measurablePings.length === 0) return Infinity
93
+ return Math.round(measurablePings.reduce((a, b) => a + b.ms, 0) / measurablePings.length)
89
94
  }
90
95
 
91
96
  // πŸ“– getVerdict: Determine a human-readable health verdict for a model.
@@ -120,16 +125,16 @@ export const getVerdict = (r) => {
120
125
  if (avg === Infinity) return 'Pending'
121
126
 
122
127
  // πŸ“– Stability-aware verdict: penalize models with good avg but terrible tail latency
123
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
128
+ const measurablePings = (r.pings || []).filter(p => measurablePingCodes.has(p.code))
124
129
  const p95 = getP95(r)
125
130
 
126
131
  if (avg < 400) {
127
132
  // πŸ“– Only flag as "Spiky" when we have enough data (β‰₯3 pings) to judge stability
128
- if (successfulPings.length >= 3 && p95 > 3000) return 'Spiky'
133
+ if (measurablePings.length >= 3 && p95 > 3000) return 'Spiky'
129
134
  return 'Perfect'
130
135
  }
131
136
  if (avg < 1000) {
132
- if (successfulPings.length >= 3 && p95 > 5000) return 'Spiky'
137
+ if (measurablePings.length >= 3 && p95 > 5000) return 'Spiky'
133
138
  return 'Normal'
134
139
  }
135
140
  if (avg < 3000) return 'Slow'
@@ -148,30 +153,30 @@ export const getUptime = (r) => {
148
153
  return Math.round((successful / r.pings.length) * 100)
149
154
  }
150
155
 
151
- // πŸ“– getP95: Calculate the 95th percentile latency from successful pings (HTTP 200).
156
+ // πŸ“– getP95: Calculate the 95th percentile latency from measurable pings (HTTP 200/401).
152
157
  // πŸ“– The p95 answers: "95% of requests are faster than this value."
153
158
  // πŸ“– A low p95 means consistently fast responses β€” a high p95 signals tail-latency spikes.
154
- // πŸ“– Returns Infinity when no successful pings exist.
159
+ // πŸ“– Returns Infinity when no measurable pings exist.
155
160
  //
156
161
  // πŸ“– Algorithm: sort latencies ascending, pick the value at ceil(N * 0.95) - 1.
157
162
  // πŸ“– Example: [100, 200, 300, 400, 5000] β†’ p95 index = ceil(5 * 0.95) - 1 = 4 β†’ 5000ms
158
163
  export const getP95 = (r) => {
159
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
160
- if (successfulPings.length === 0) return Infinity
161
- const sorted = successfulPings.map(p => p.ms).sort((a, b) => a - b)
164
+ const measurablePings = (r.pings || []).filter(p => measurablePingCodes.has(p.code))
165
+ if (measurablePings.length === 0) return Infinity
166
+ const sorted = measurablePings.map(p => p.ms).sort((a, b) => a - b)
162
167
  const idx = Math.ceil(sorted.length * 0.95) - 1
163
168
  return sorted[Math.max(0, idx)]
164
169
  }
165
170
 
166
- // πŸ“– getJitter: Calculate latency standard deviation (Οƒ) from successful pings.
171
+ // πŸ“– getJitter: Calculate latency standard deviation (Οƒ) from measurable pings.
167
172
  // πŸ“– Low jitter = predictable response times. High jitter = erratic, spiky latency.
168
- // πŸ“– Returns 0 when fewer than 2 successful pings (can't compute variance from 1 point).
173
+ // πŸ“– Returns 0 when fewer than 2 measurable pings (can't compute variance from 1 point).
169
174
  // πŸ“– Uses population Οƒ (divides by N, not N-1) since we have ALL the data, not a sample.
170
175
  export const getJitter = (r) => {
171
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
172
- if (successfulPings.length < 2) return 0
173
- const mean = successfulPings.reduce((a, b) => a + b.ms, 0) / successfulPings.length
174
- const variance = successfulPings.reduce((sum, p) => sum + (p.ms - mean) ** 2, 0) / successfulPings.length
176
+ const measurablePings = (r.pings || []).filter(p => measurablePingCodes.has(p.code))
177
+ if (measurablePings.length < 2) return 0
178
+ const mean = measurablePings.reduce((a, b) => a + b.ms, 0) / measurablePings.length
179
+ const variance = measurablePings.reduce((sum, p) => sum + (p.ms - mean) ** 2, 0) / measurablePings.length
175
180
  return Math.round(Math.sqrt(variance))
176
181
  }
177
182
 
@@ -190,14 +195,14 @@ export const getJitter = (r) => {
190
195
  // Model B: avg 400ms, p95 650ms (boringly consistent) β†’ score ~85
191
196
  // In real usage, Model B FEELS faster because it doesn't randomly stall.
192
197
  export const getStabilityScore = (r) => {
193
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
194
- if (successfulPings.length === 0) return -1
198
+ const measurablePings = (r.pings || []).filter(p => measurablePingCodes.has(p.code))
199
+ if (measurablePings.length === 0) return -1
195
200
 
196
201
  const p95 = getP95(r)
197
202
  const jitter = getJitter(r)
198
203
  const uptime = getUptime(r)
199
- const spikeCount = successfulPings.filter(p => p.ms > 3000).length
200
- const spikeRate = spikeCount / successfulPings.length
204
+ const spikeCount = measurablePings.filter(p => p.ms > 3000).length
205
+ const spikeRate = spikeCount / measurablePings.length
201
206
 
202
207
  // πŸ“– Normalize each component to 0–100 (higher = better)
203
208
  const p95Score = Math.max(0, Math.min(100, 100 * (1 - p95 / 5000)))