diff --git a/docs/guides/metrics.md b/docs/guides/metrics.md index ecf33ec..0698dc2 100644 --- a/docs/guides/metrics.md +++ b/docs/guides/metrics.md @@ -34,7 +34,8 @@ the URL; refreshes preserve table controls and chart selection. The timeline defaults to **Average lobbies running**, split into **With players**, **Empty spare**, and **Unknown**. Each bucket divides those lobby-hours by its captured wall time, so changing lobby counts remain visible. Collection gaps are -excluded from that denominator. Switch to the share view to compare proportions, +excluded from that denominator. The average-lobby axis uses whole-number ticks; +period details preserve fractional averages. Switch to the share view to compare proportions, or hours to compare absolute lobby time. Periods without captured time remain gaps. The timeline has labelled axes and marks partial edge periods. Select a bar or use @@ -77,6 +78,11 @@ Game-wide policies apply independently to each supported map variant. Each signa links to the policy on **Fleet → Autoscaling**; a map without a policy gets a link to add one. Signals are advisory and never change a policy or a lobby. +A global pause appears once above the signals. Historical shortage signals remain +visible while the fleet is paused, including suggestions to review the maximum +or spare target. Review these findings and current settings before resuming. +An individual policy's own pause or disabled state still prompts a policy review. + | Recorded condition | Suggested action | | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | No empty lobby for at least 2% of known map time, mostly at or above the current maximum | Review raising the policy maximum. | @@ -137,5 +143,6 @@ before scope filtering, so samples from other groups still end a batch interval. their own `capturedHours`. Map rows expose `noEmpty`, `availabilityObserved`, and `availabilityUnknown` durations, plus current `policy` context and `noEmptyAtMaximum` / `noEmptyBelowMaximum` durations. `pooledAvailability` combines -map durations and coverage. Policy edits and global pause changes invalidate +map durations and coverage. `autoscalingPaused` reports the current global pause +separately from each policy's own `pausedReason`. Policy edits and global pause changes invalidate cached metrics reports. diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 733427d..d7f7301 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -130,6 +130,8 @@ export interface MetricsData { firstRetainedSampleAt: number | null; latestSampleAt: number | null; registeredServerCount: number; + /** Current fleet-wide pause, separate from each policy's own state. */ + autoscalingPaused: boolean; scope: MetricsScope; scopeOptions: { games: string[]; maps: { game: string; map: string }[] }; scopeFirstSampleAt: number | null; diff --git a/src/lib/server/fleet/metrics.ts b/src/lib/server/fleet/metrics.ts index 5fde9b8..7ec1f22 100644 --- a/src/lib/server/fleet/metrics.ts +++ b/src/lib/server/fleet/metrics.ts @@ -123,11 +123,7 @@ export function readMetrics(input: MetricsDays | MetricsQuery, now = Date.now()) minimum, maximum, spares, - pausedReason: globalPaused - ? ['Autoscaling is globally paused.', row.paused_reason] - .filter(Boolean) - .join(' ') - : row.paused_reason + pausedReason: row.paused_reason } }; }); @@ -441,6 +437,7 @@ export function readMetrics(input: MetricsDays | MetricsQuery, now = Date.now()) since, now, ...metadata, + autoscalingPaused: globalPaused, scope, scopeOptions, summary, diff --git a/src/lib/ui/components/metrics/MetricsChart.svelte b/src/lib/ui/components/metrics/MetricsChart.svelte index ad2b9f4..695f3ac 100644 --- a/src/lib/ui/components/metrics/MetricsChart.svelte +++ b/src/lib/ui/components/metrics/MetricsChart.svelte @@ -6,6 +6,7 @@ import { metricDuration, metricAverageLobbies, + metricLobbyAxis, metricNumber, metricPercent, metricPeriodLabels @@ -34,8 +35,8 @@ }); const selected = $derived(points[selectedIndex]); const selectedValue = $derived(selected ? String(selected.at) : ''); - const ceiling = $derived.by(() => { - if (mode === 'percent') return 100; + const axis = $derived.by(() => { + if (mode === 'percent') return { ceiling: 100, ticks: [100, 75, 50, 25, 0] }; const max = Math.max( 0, ...points.map((point) => @@ -45,15 +46,20 @@ : point.observed + point.unknown ) ); - if (!max) return 1; - const magnitude = 10 ** Math.floor(Math.log10(max)); - return Math.ceil(max / (magnitude / 2)) * (magnitude / 2); + if (mode === 'average') return metricLobbyAxis(max); + const magnitude = max ? 10 ** Math.floor(Math.log10(max)) : 1; + const ceiling = max ? Math.ceil(max / (magnitude / 2)) * (magnitude / 2) : 1; + return { + ceiling, + ticks: [1, 0.75, 0.5, 0.25, 0].map((fraction) => ceiling * fraction) + }; }); + const ceiling = $derived(axis.ceiling); const tick = new Intl.NumberFormat('en-US', { maximumSignificantDigits: 3 }); const ticks = $derived( - [1, 0.75, 0.5, 0.25, 0].map((fraction) => ({ - fraction, - value: `${tick.format(ceiling * fraction)}${mode === 'percent' ? '%' : ''}` + axis.ticks.map((value) => ({ + fraction: value / ceiling, + value: `${tick.format(value)}${mode === 'percent' ? '%' : ''}` })) ); const periodOptions = $derived( diff --git a/src/lib/ui/components/metrics/MetricsExplore.svelte b/src/lib/ui/components/metrics/MetricsExplore.svelte index 44254e9..e2c42d1 100644 --- a/src/lib/ui/components/metrics/MetricsExplore.svelte +++ b/src/lib/ui/components/metrics/MetricsExplore.svelte @@ -70,6 +70,10 @@ are applied to historical lobby counts; these are not records of the policy active then. No reported empty lobby does not prove that players were turned away.
+ {#if report.autoscalingPaused}+ Autoscaling is globally paused. Review the shortage signals and current settings before + resuming in Autoscaling. +
{/if} {#if highlighted.length}{#each highlighted as suggestion}{@render finding(suggestion)}{/each} {:else}No policy changes meet the evidence threshold in this period.
{/if} {#if limited}@@ -124,8 +128,9 @@ empty. Player limits are not needed for these signals.
- Time with players as a share of known server time, grouped in {heatmap.timeZone.replaceAll( + Time with players as a share of known lobby time, grouped in {heatmap.timeZone.replaceAll( '_', ' ' )}. @@ -233,7 +233,7 @@