@jeffusion/bungee-core 6.2.1 → 6.3.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.
@@ -10,30 +10,44 @@
10
10
  import type { TpsRange } from '../contract';
11
11
  import type { TpsResource, TpsState } from './stats-resource';
12
12
  import { getTpsResource } from './stats';
13
- import { endpointIdentity, formatTps, rankedEndpoints } from './presentation';
13
+ import { endpointIdentity, endpointLabel, formatTps, rankedEndpoints, rankedModels } from './presentation';
14
+ import Sparkline from '$components/dashboard/Sparkline.svelte';
14
15
 
15
16
  let { pluginName, selectedRange, onHeaderChange, mode }:
16
- { pluginName: string; selectedRange: TimeRange; onHeaderChange?: NativeWidgetHeaderChange; mode: 'overview' | 'endpoints' } = $props();
17
+ { pluginName: string; selectedRange: TimeRange; onHeaderChange?: NativeWidgetHeaderChange; mode: 'overview' | 'endpoints' | 'models' } = $props();
17
18
  let state: TpsState = $state({ data: null, busy: true, error: false });
18
19
  let resource: TpsResource | undefined = $state();
19
20
  const range: TpsRange = $derived(selectedRange === '1h' || selectedRange === '12h' ? selectedRange : '24h');
20
21
  const t = (key: string, values?: Record<string, string | number>) => $isLoading ? '' : getPluginText(key, pluginName,
21
22
  (id, options) => $_(id, { ...options, values }));
22
- const rows = $derived(rankedEndpoints(state.data?.endpoints ?? []));
23
- const maximum = $derived(rows.reduce((max, row) => Math.max(max, row.tps ?? 0), 0));
23
+ const maximum = $derived(Math.max(100,
24
+ (state.data?.endpoints ?? []).reduce((max, row) => Math.max(max, row.tps ?? 0), 0),
25
+ (state.data?.models ?? []).reduce((max, row) => Math.max(max, row.tps ?? 0), 0)));
26
+ const sampleCount = $derived(state.data?.total.sampleCount ?? 0);
27
+ const estimatedCount = $derived(state.data?.total.estimatedSampleCount ?? 0);
28
+ const trendValues = $derived((state.data?.history ?? []).map(point => point.tps ?? NaN));
24
29
  const services = $derived($configStore?.services ?? []);
30
+ const routeLabels = $derived(new Map($routesStore.map(route => [route.id,
31
+ 'name' in route && typeof route.name === 'string' && route.name.trim() ? route.name.trim() : route.path || '—'])));
25
32
  const labels = $derived.by(() => {
26
- const result = new Map<string, { route: string; endpoint: string }>();
33
+ const result = new Map<string, { route: string; endpoint: string; description: string }>();
27
34
  const editorServices = services.map(toEditorService);
28
35
  for (const route of $routesStore) {
29
36
  const endpoints = resolveRouteEndpoints(toEditorRoute(route, services), editorServices);
30
37
  for (const endpoint of endpoints) if (endpoint._uid) result.set(JSON.stringify([route.id, endpoint._uid]), {
31
- route: 'name' in route && typeof route.name === 'string' ? route.name.trim() || route.path || route.id : route.path || route.id,
32
- endpoint: endpoint.description?.trim() || endpointIdentity(endpoint._uid),
38
+ route: routeLabels.get(route.id) ?? '—',
39
+ endpoint: endpointLabel(endpoint.target, endpoint._uid),
40
+ description: endpoint.description?.trim() ?? '',
33
41
  });
34
42
  }
35
43
  return result;
36
44
  });
45
+ const rows = $derived(mode === 'models'
46
+ ? rankedModels(state.data?.models ?? []).map(row => ({ ...row, key: row.model, label: row.model, description: '' }))
47
+ : rankedEndpoints(state.data?.endpoints ?? []).map(row => {
48
+ const label = labels.get(JSON.stringify([row.routeId, row.endpointId]));
49
+ return { ...row, key: row.endpointId, label: label?.endpoint ?? endpointLabel(undefined, row.endpointId), description: label?.description ?? '' };
50
+ }));
37
51
  $effect(() => {
38
52
  const next = getTpsResource(pluginName, range);
39
53
  resource = next;
@@ -42,7 +56,8 @@
42
56
  $effect(() => {
43
57
  const report = onHeaderChange;
44
58
  const header = { summary: range, refresh: { label: t('ui.refresh'), busy: state.busy,
45
- disabled: !resource, run: () => { void resource?.refresh(); } } };
59
+ disabled: !resource, run: () => { void resource?.refresh(); } },
60
+ ...(mode === 'overview' && state.data?.total.sampleCount ? { footer: sampleFooter } : {}) };
46
61
  untrack(() => report?.(header));
47
62
  });
48
63
  onMount(() => {
@@ -51,65 +66,76 @@
51
66
  });
52
67
  </script>
53
68
 
54
- <div class="tps-content min-w-0 w-full" data-testid={`tps-${mode}`} aria-busy={state.busy}>
69
+ {#snippet sampleFooter()}
70
+ {#if sampleCount}
71
+ <div class="tps-samples" data-testid="tps-total-samples">
72
+ <span>{t('ui.sampleCount')} <b class="nx-display">{sampleCount.toLocaleString()}</b></span>
73
+ {#if estimatedCount > 0}<span>{t('ui.estimatedCount')} <b class="nx-display">{estimatedCount.toLocaleString()}</b></span>{/if}
74
+ </div>
75
+ {/if}
76
+ {/snippet}
77
+
78
+ <div class="tps-content min-w-0 w-full" class:tps-overview={mode === 'overview'} data-testid={`tps-${mode}`} aria-busy={state.busy}>
55
79
  {#if $isLoading}
56
80
  <span class="nx-metric text-zinc-50">—</span>
57
81
  {:else}
58
82
  {#if mode === 'overview'}
59
- <div class="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1.5">
83
+ <div class="kpi-metric-row flex w-full min-w-0 items-center justify-between gap-3">
84
+ <div class="kpi-metric-group flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1.5">
60
85
  <span class="nx-metric tps-value text-zinc-50" data-testid="tps-total-value">{formatTps(state.data?.total.sampleCount ? state.data.total.tps : null)}</span>
61
- <span class="font-mono text-sm text-zinc-400">tokens/s</span>
86
+ <span class="nx-label">TOKENS/S</span>
87
+ </div>
88
+ <div class="kpi-icon flex shrink-0 items-center"><Sparkline values={trendValues} tone="orange" /></div>
62
89
  </div>
63
- <p class="mt-3 text-sm text-zinc-400">{t('ui.weightedHint')}</p>
64
90
  {/if}
65
91
  {#if state.error}
66
92
  <p role="alert" class="text-sm text-red-300" data-testid="tps-error">{t('ui.loadFailed')}</p>
67
93
  {:else if state.busy && !state.data}
68
94
  <p role="status" class="text-sm text-zinc-400">{t('ui.loading')}</p>
69
95
  {:else if state.data}
70
- {#if !state.data.total.sampleCount}
96
+ {#if mode === 'overview' ? !state.data.total.sampleCount : !rows.length}
71
97
  <p class="text-sm text-zinc-400" data-testid="tps-empty">{t('ui.noData')}</p>
72
- {:else if mode === 'overview'}
73
- <p class="mt-3 font-mono text-sm text-zinc-300" data-testid="tps-total-samples">{t('ui.samples', {
74
- count: state.data.total.sampleCount, estimated: state.data.total.estimatedSampleCount })}</p>
75
- {:else}
98
+ {:else if mode !== 'overview'}
99
+ <p class="ranking-unit">TOKENS/S</p>
76
100
  <!-- svelte-ignore a11y_no_noninteractive_tabindex (A scrolling region needs keyboard focus so every endpoint remains reachable.) -->
77
- <div class="endpoint-list" role="region" aria-label={t('ui.ranking')} data-testid="tps-endpoint-list" tabindex="0">
78
- <ol class="endpoint-items">
79
- {#each rows as row (JSON.stringify([row.routeId, row.endpointId]))}
80
- {@const label = labels.get(JSON.stringify([row.routeId, row.endpointId]))}
81
- <li class="endpoint-row" data-testid="tps-endpoint-row" data-route-id={row.routeId} data-endpoint-id={endpointIdentity(row.endpointId)}>
82
- <div class="endpoint-heading">
83
- <div class="min-w-0">
84
- <p class="endpoint-label text-sm text-zinc-200">{label?.endpoint ?? endpointIdentity(row.endpointId)}</p>
85
- <p class="endpoint-label mt-1 text-xs text-zinc-400">{label?.route ?? row.routeId}</p>
86
- </div>
87
- <p class="font-mono text-sm text-zinc-50">{formatTps(row.sampleCount ? row.tps : null)} <span class="text-xs text-zinc-400">tokens/s</span></p>
101
+ <div class="ranking-list" role="region" aria-label={t(mode === 'models' ? 'ui.modelRanking' : 'ui.ranking')} data-testid={mode === 'models' ? 'tps-model-list' : 'tps-endpoint-list'} tabindex="0">
102
+ <ol class="ranking-items">
103
+ {#each rows as row, index (JSON.stringify([row.routeId, row.key]))}
104
+ <li class="ranking-row" data-testid={mode === 'models' ? 'tps-model-row' : 'tps-endpoint-row'} data-route-id={row.routeId}
105
+ data-endpoint-id={mode === 'endpoints' ? endpointIdentity(row.key) : undefined} data-model={mode === 'models' ? row.key : undefined}>
106
+ <span class="ranking-position nx-display" aria-hidden="true">{index + 1}</span>
107
+ <div class="ranking-identity">
108
+ <p class="ranking-name" title={row.description || undefined}>{row.label}</p>
109
+ <p class="ranking-route">{routeLabels.get(row.routeId) ?? '—'}</p>
88
110
  </div>
89
- <div class="mt-2 h-1.5 bg-carbon-700" aria-hidden="true"><div class="h-full bg-nexus-500" style:width={`${maximum && row.tps !== null ? Math.max(0, row.tps / maximum * 100) : 0}%`}></div></div>
90
- <p class="mt-2 font-mono text-xs text-zinc-400">{t('ui.samples', { count: row.sampleCount, estimated: row.estimatedSampleCount })}</p>
111
+ <span class="ranking-value nx-display" title={t('ui.samples', { count: row.sampleCount, estimated: row.estimatedSampleCount })}>{formatTps(row.sampleCount ? row.tps : null)}</span>
112
+ <div class="ranking-track" aria-hidden="true"><span class="ranking-bar" style:width={`${(row.tps ?? 0) / maximum * 100}%`}></span></div>
91
113
  </li>
92
114
  {/each}
93
115
  </ol>
94
116
  </div>
95
117
  {/if}
96
- {#if state.data.reportingIncomplete}
97
- <p class="mt-3 text-sm text-amber-400" role="status" data-testid="tps-incomplete">{t('ui.incomplete')}</p>
98
- {/if}
99
118
  {/if}
100
119
  {/if}
101
120
  </div>
102
121
 
103
122
  <style>
104
123
  .tps-content { display: flex; flex-direction: column; gap: 6px; }
124
+ .tps-content:not(.tps-overview) { height: 100%; min-height: 0; }
125
+ .tps-overview { justify-content: center; min-height: 64px; }
105
126
  .tps-value { font-size: clamp(28px, 4cqw, 42px); overflow-wrap: anywhere; }
106
- .endpoint-list { max-height: 360px; overflow-y: auto; overscroll-behavior: contain; }
107
- .endpoint-items { list-style: none; margin: 0; padding: 0; }
108
- .endpoint-row { padding: 12px 0; border-bottom: 1px solid var(--nx-edge); }
109
- .endpoint-row:first-child { padding-top: 0; }
110
- .endpoint-row:last-child { border-bottom: 0; }
111
- .endpoint-heading { display: flex; flex-wrap: wrap; align-items: start; justify-content: space-between; gap: 6px 18px; }
112
- .endpoint-heading > div { flex: 1 1 180px; }
113
- .endpoint-heading > p { flex: 0 1 auto; overflow-wrap: anywhere; }
114
- .endpoint-label { overflow-wrap: anywhere; }
127
+ .tps-samples { display: flex; flex-wrap: wrap; gap: 6px 18px; font-family: var(--font-mono, 'DM Mono', monospace); font-size: 10px; letter-spacing: .08em; color: var(--nx-text-mute); }
128
+ .tps-samples b { margin-left: 4px; font-weight: 400; font-variant-numeric: tabular-nums; color: var(--nx-text); }
129
+ .ranking-unit { margin: 0; text-align: right; font: 10px var(--font-mono, 'DM Mono', monospace); letter-spacing: .08em; color: var(--nx-text-dim); }
130
+ .ranking-list { flex: 1; min-height: 0; max-height: 480px; overflow-y: auto; overscroll-behavior: contain; }
131
+ .ranking-items { list-style: none; margin: 0; padding: 0; }
132
+ .ranking-row { display: grid; grid-template-columns: 20px minmax(0, 1fr) max-content; align-items: center; gap: 6px 12px; padding: 12px 0; border-bottom: 1px solid var(--nx-edge); }
133
+ .ranking-row:last-child { border-bottom: 0; }
134
+ .ranking-position { grid-column: 1; grid-row: 1 / 3; color: var(--nx-text-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
135
+ .ranking-identity { min-width: 0; }
136
+ .ranking-name { margin: 0; font-size: 14px; font-weight: 500; line-height: 1.4; color: var(--nx-text); overflow-wrap: anywhere; }
137
+ .ranking-route { margin: 3px 0 0; font: 12px/1.4 var(--font-mono, 'DM Mono', monospace); color: var(--nx-text-dim); overflow-wrap: anywhere; }
138
+ .ranking-value { grid-column: 3; grid-row: 1 / 3; font-size: 20px; font-variant-numeric: tabular-nums; color: var(--nx-text); }
139
+ .ranking-track { grid-column: 2; height: 6px; }
140
+ .ranking-bar { display: block; height: 100%; background: var(--nx-accent); }
115
141
  </style>
@@ -1,4 +1,4 @@
1
- import type { TpsEndpointStats } from '../contract';
1
+ import type { TpsEndpointStats, TpsModelStats } from '../contract';
2
2
 
3
3
  export function formatTps(value: number | null | undefined): string {
4
4
  return typeof value === 'number' && Number.isFinite(value) && value >= 0
@@ -10,7 +10,21 @@ export function endpointIdentity(id: string): string {
10
10
  return /^[a-zA-Z0-9_-]+$/.test(id) ? id : 'endpoint';
11
11
  }
12
12
 
13
+ /** Match the dashboard's host labels without exposing URL credentials, paths or queries. */
14
+ export function endpointLabel(target: string | undefined, id: string): string {
15
+ try {
16
+ const url = new URL(target ?? '');
17
+ if (url.protocol === 'http:' || url.protocol === 'https:') return url.host;
18
+ } catch { /* Deleted or managed endpoints may have no URL. */ }
19
+ return `endpoint · ${endpointIdentity(id).slice(0, 8)}`;
20
+ }
21
+
13
22
  export function rankedEndpoints(rows: readonly TpsEndpointStats[]): TpsEndpointStats[] {
14
23
  return [...rows].sort((a, b) => (b.tps ?? -1) - (a.tps ?? -1)
15
24
  || a.routeId.localeCompare(b.routeId) || a.endpointId.localeCompare(b.endpointId));
16
25
  }
26
+
27
+ export function rankedModels(rows: readonly TpsModelStats[]): TpsModelStats[] {
28
+ return [...rows].sort((a, b) => (b.tps ?? -1) - (a.tps ?? -1)
29
+ || a.routeId.localeCompare(b.routeId) || a.model.localeCompare(b.model));
30
+ }
@@ -14,8 +14,12 @@ function validAggregate(value: TpsAggregate): boolean {
14
14
 
15
15
  export function validateStats(data: TpsStatsResponse, range: TpsRange): TpsStatsResponse {
16
16
  if (!data || data.range !== range || !Number.isFinite(data.asOfMs) || !validAggregate(data.total)
17
- || typeof data.reportingIncomplete !== 'boolean' || !Array.isArray(data.endpoints)
18
- || !data.endpoints.every(row => validAggregate(row) && typeof row.routeId === 'string' && typeof row.endpointId === 'string')) {
17
+ || typeof data.reportingIncomplete !== 'boolean' || !Array.isArray(data.endpoints) || !Array.isArray(data.models) || !Array.isArray(data.history)
18
+ || !data.history.every((row, index) => validAggregate(row) && Number.isSafeInteger(row.bucketStartMs) && row.bucketStartMs >= 0
19
+ && (index === 0 || row.bucketStartMs > data.history[index - 1]!.bucketStartMs))
20
+ || !data.endpoints.every(row => validAggregate(row) && typeof row.routeId === 'string' && typeof row.endpointId === 'string')
21
+ || !data.models.every(row => validAggregate(row) && typeof row.routeId === 'string' && !!row.routeId.trim()
22
+ && typeof row.model === 'string' && !!row.model.trim())) {
19
23
  throw new Error('invalid_tps_stats');
20
24
  }
21
25
  return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffusion/bungee-core",
3
- "version": "6.2.1",
3
+ "version": "6.3.0",
4
4
  "description": "Bungee reverse proxy core engine",
5
5
  "main": "./dist/main.js",
6
6
  "types": "./dist/main.d.ts",
@@ -16,7 +16,7 @@
16
16
  "@iconify-json/lucide": "^1.2.145",
17
17
  "@iconify-json/tabler": "^1.2.42",
18
18
  "@iconify/utils": "^3.1.7",
19
- "@jeffusion/bungee-types": "^6.2.1",
19
+ "@jeffusion/bungee-types": "^6.3.0",
20
20
  "@types/lodash-es": "^4.17.12",
21
21
  "@types/ws": "^8.18.1",
22
22
  "acorn": "^8.15.0",