Files
electric-cherry/src/lib/metrics-insights.ts
T

118 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { MapMetrics } from './metrics.js';
import { metricDuration, metricPercent } from './ui/metrics.js';
/** Product hypotheses for advisory experiments; these are not demand forecasts. */
export const HOSTING_RULES = {
pressureExpansion: 0.2,
occupancyExpansion: 0.8,
idleUtilization: 0.1,
quietPressure: 0.01,
minimumEvidenceHours: 1
} as const;
export interface MetricRecommendation {
game: string;
map: string;
kind: 'expand' | 'consolidate' | 'capacity' | 'watch';
title: string;
reason: string;
evidence: string;
outcome: string;
tentative: boolean;
}
/** Apply the same visible rules to all selected maps, including sparse observations. */
export function metricRecommendations(maps: MapMetrics[]): MetricRecommendation[] {
return maps
.map((map) => {
const utilization = map.observed > 0 ? map.active / map.observed : null;
const pressure = map.capacityObserved > 0 ? map.full / map.capacityObserved : null;
const knownShare =
map.observed + map.unknown > 0 ? map.observed / (map.observed + map.unknown) : 0;
const base = {
game: map.game,
map: map.map,
evidence:
`${metricDuration(map.active)} with players / ${metricDuration(map.observed)} known server time (${metricPercent(map.active, map.observed)}). ` +
(pressure === null
? 'Capacity unavailable.'
: `All lobbies full ${metricDuration(map.full)} / ${metricDuration(map.capacityObserved)} known map time (${metricPercent(map.full, map.capacityObserved)}).`),
tentative:
map.observed < HOSTING_RULES.minimumEvidenceHours ||
pressure === null ||
map.capacityObserved < HOSTING_RULES.minimumEvidenceHours ||
knownShare < 0.8
};
if (utilization === null)
return {
...base,
kind: 'capacity' as const,
title:
map.unknown > 0
? 'Check occupancy reporting'
: 'Let observations accumulate',
reason:
map.unknown > 0
? 'Captured status is unknown. Check a server’s telemetry or RCON diagnostics before choosing a hosting change.'
: 'This map has no elapsed observations in the selected window. Wait for another status check or try a wider range.',
outcome: 'Watch for known server time in the next report.',
tentative: true
};
if (pressure !== null && pressure >= HOSTING_RULES.pressureExpansion)
return {
...base,
kind: 'expand' as const,
title: 'Trial one additional lobby',
reason: 'Recorded lobbies were all full for at least 20% of known capacity time. Try one extra lobby during busy hours.',
outcome:
'Compare all-full time and use of the additional lobby over the next comparable period.'
};
if (pressure === null && utilization >= HOSTING_RULES.occupancyExpansion)
return {
...base,
kind: 'expand' as const,
title: 'Trial an extra lobby during busy hours',
reason: 'Players were present for at least 80% of known server time. Capacity is unknown, so this experiment uses occupancy as a proxy, not evidence of saturation.',
outcome:
'Check whether players use the extra lobby, and measure capacity if the server reports it.',
tentative: true
};
if (
utilization <= HOSTING_RULES.idleUtilization &&
(pressure === null || pressure < HOSTING_RULES.quietPressure)
)
return {
...base,
kind: 'consolidate' as const,
title: 'Trial less idle hosting',
reason: 'Players were present for at most 10% of known time, with less than 1% measured pressure or unavailable capacity. Review one idle lobby or a shorter hosting window; preserve access to this map.',
outcome:
'Watch for increased all-full time or loss of player-hours, and restore the lineup if the trial hurts availability.'
};
if (pressure === null)
return {
...base,
kind: 'capacity' as const,
title: 'Check capacity during busy hours',
reason: 'Occupancy is recorded, but player limits are unavailable. Check capacity reporting while keeping the current lineup.',
outcome:
'Look for known-capacity time and all-full periods before the next hosting trial.'
};
return {
...base,
kind: 'watch' as const,
title: 'Keep the lineup and watch peaks',
reason: 'These observations do not trigger the expansion or idle-hosting rules. Keep the current lineup for another comparison period.',
outcome: 'Watch whether occupied time and measured capacity pressure change.'
};
})
.sort((a, b) => {
const priority = { expand: 0, consolidate: 1, capacity: 2, watch: 3 };
return (
priority[a.kind] - priority[b.kind] ||
a.map.localeCompare(b.map) ||
a.game.localeCompare(b.game)
);
});
}