Preserve shortage signals during global pause (Refs #137)
CI / checks (push) Successful in 4m47s
CI / checks (push) Successful in 4m47s
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
{#if report.autoscalingPaused}<p class="notice">
|
||||
Autoscaling is globally paused. Review the shortage signals and current settings before
|
||||
resuming in <a href="/autoscaling">Autoscaling</a>.
|
||||
</p>{/if}
|
||||
{#if highlighted.length}{#each highlighted as suggestion}{@render finding(suggestion)}{/each}
|
||||
{:else}<p class="empty">No policy changes meet the evidence threshold in this period.</p>{/if}
|
||||
{#if limited}<p class="notice">
|
||||
@@ -124,8 +128,9 @@
|
||||
empty. Player limits are not needed for these signals.
|
||||
</li>
|
||||
<li>
|
||||
Maps without a current policy suggest adding one. Paused or disabled policies prompt
|
||||
a review before resuming or enabling them.
|
||||
Maps without a current policy suggest adding one. Individually paused or disabled
|
||||
policies prompt a review before resuming or enabling them. A global pause does not
|
||||
replace shortage signals.
|
||||
</li>
|
||||
<li>
|
||||
All-unknown occupancy prompts a reporting check. Other cases suggest watching
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
);
|
||||
}
|
||||
function description(cell: Cell) {
|
||||
return `${weekdays[cell.weekday]}, ${hourLabel(cell.hour)}: ${cell.observed ? `${metricPercent(cell.active, cell.observed)} with players during ${metricDuration(cell.observed)} known server time` : cell.unknown ? `${metricDuration(cell.unknown)} unknown server time` : 'No captured time'}. ${cell.distinctDates} distinct dates.`;
|
||||
return `${weekdays[cell.weekday]}, ${hourLabel(cell.hour)}: ${cell.observed ? `${metricPercent(cell.active, cell.observed)} with players during ${metricDuration(cell.observed)} known lobby time` : cell.unknown ? `${metricDuration(cell.unknown)} unknown lobby time` : 'No captured time'}. ${cell.distinctDates} distinct dates.`;
|
||||
}
|
||||
function select(day: number, hour: number) {
|
||||
selectedKey = day * 24 + hour;
|
||||
@@ -102,7 +102,7 @@
|
||||
<div>
|
||||
<h2 id={`${id}-heatmap-title`}>Activity by weekday and hour</h2>
|
||||
<p>
|
||||
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 @@
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Unknown server time</dt>
|
||||
<dt>Unknown lobby time</dt>
|
||||
<dd>{metricDuration(selected.unknown)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -55,6 +55,7 @@ function validReport(value: unknown): value is MetricsData {
|
||||
return false;
|
||||
if (
|
||||
!numeric(value.registeredServerCount) ||
|
||||
typeof value.autoscalingPaused !== 'boolean' ||
|
||||
![
|
||||
'firstRetainedSampleAt',
|
||||
'latestSampleAt',
|
||||
|
||||
@@ -38,6 +38,22 @@ export function metricAverageLobbies(lobbyHours: number, capturedHours: number):
|
||||
return lobbyHours / capturedHours;
|
||||
}
|
||||
|
||||
/** Use up to four whole-lobby intervals while retaining fractional averages in the data. */
|
||||
export function metricLobbyAxis(maximumLobbies: number) {
|
||||
const maximum = Number.isFinite(maximumLobbies) ? Math.max(1, maximumLobbies) : 1;
|
||||
const targetStep = Math.max(1, maximum / 4);
|
||||
const magnitude = 10 ** Math.floor(Math.log10(targetStep));
|
||||
const step =
|
||||
[1, 2, 5, 10]
|
||||
.map((multiple) => multiple * magnitude)
|
||||
.find((value) => value >= targetStep) ?? 10 * magnitude;
|
||||
const intervals = Math.ceil(maximum / step);
|
||||
return {
|
||||
ceiling: intervals * step,
|
||||
ticks: Array.from({ length: intervals + 1 }, (_, index) => (intervals - index) * step)
|
||||
};
|
||||
}
|
||||
|
||||
export function metricPercent(value: number, total: number): string {
|
||||
if (!Number.isFinite(value) || !Number.isFinite(total) || total <= 0) return '—';
|
||||
const percent = (100 * value) / total;
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
<summary>About this report</summary>
|
||||
<div class="context">
|
||||
{#if captured > 0}<span
|
||||
><strong>{metricDuration(report.summary.observed)}</strong> known server time</span
|
||||
><strong>{metricDuration(report.summary.observed)}</strong> known lobby time</span
|
||||
><span
|
||||
><strong>{metricDuration(report.summary.unknown)}</strong> unknown status</span
|
||||
>{/if}
|
||||
|
||||
Reference in New Issue
Block a user