Preserve shortage signals during global pause (Refs #137)
CI / checks (push) Successful in 4m47s

This commit is contained in:
2026-10-10 16:11:24 -04:00
parent 33da4482a6
commit ea932ec563
14 changed files with 154 additions and 26 deletions
@@ -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>
+1
View File
@@ -55,6 +55,7 @@ function validReport(value: unknown): value is MetricsData {
return false;
if (
!numeric(value.registeredServerCount) ||
typeof value.autoscalingPaused !== 'boolean' ||
![
'firstRetainedSampleAt',
'latestSampleAt',
+16
View File
@@ -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;