Make metrics reflect autoscaling policies (Refs #137)
CI / checks (push) Successful in 4m49s
CI / checks (push) Successful in 4m49s
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { createTimeFormatter } from '$lib/time-zone.js';
|
||||
import {
|
||||
metricDuration,
|
||||
metricAverageLobbies,
|
||||
metricNumber,
|
||||
metricPercent,
|
||||
metricPeriodLabels
|
||||
@@ -21,9 +22,10 @@
|
||||
const contextSearch = $derived(
|
||||
(page.state as { metricsTableSearch?: string }).metricsTableSearch ?? page.url.search
|
||||
);
|
||||
const mode = $derived(
|
||||
new URLSearchParams(contextSearch).get('chartMode') === 'hours' ? 'hours' : 'percent'
|
||||
);
|
||||
const mode = $derived.by(() => {
|
||||
const value = new URLSearchParams(contextSearch).get('chartMode');
|
||||
return value === 'hours' || value === 'percent' ? value : 'average';
|
||||
});
|
||||
const selectedIndex = $derived.by(() => {
|
||||
const existing = points.findIndex((point) => point.at === selectedAt);
|
||||
if (existing >= 0) return existing;
|
||||
@@ -34,7 +36,15 @@
|
||||
const selectedValue = $derived(selected ? String(selected.at) : '');
|
||||
const ceiling = $derived.by(() => {
|
||||
if (mode === 'percent') return 100;
|
||||
const max = Math.max(0, ...points.map((point) => point.observed + point.unknown));
|
||||
const max = Math.max(
|
||||
0,
|
||||
...points.map((point) =>
|
||||
mode === 'average'
|
||||
? (metricAverageLobbies(point.observed + point.unknown, point.capturedHours) ??
|
||||
0)
|
||||
: point.observed + point.unknown
|
||||
)
|
||||
);
|
||||
if (!max) return 1;
|
||||
const magnitude = 10 ** Math.floor(Math.log10(max));
|
||||
return Math.ceil(max / (magnitude / 2)) * (magnitude / 2);
|
||||
@@ -55,10 +65,10 @@
|
||||
const periodLabels = $derived(metricPeriodLabels(points, days, timeZone));
|
||||
const captured = $derived(selected ? selected.observed + selected.unknown : 0);
|
||||
|
||||
function setMode(value: 'hours' | 'percent') {
|
||||
function setMode(value: 'average' | 'hours' | 'percent') {
|
||||
const url = new URL(page.url);
|
||||
url.search = contextSearch;
|
||||
if (value === 'percent') url.searchParams.delete('chartMode');
|
||||
if (value === 'average') url.searchParams.delete('chartMode');
|
||||
else url.searchParams.set('chartMode', value);
|
||||
replaceState(url, { ...page.state, metricsTableSearch: url.search });
|
||||
}
|
||||
@@ -66,12 +76,27 @@
|
||||
selectedAt = at;
|
||||
const point = points.find((item) => item.at === at);
|
||||
if (!point) return;
|
||||
announcement = `${format.period(point.startAt)} to ${format.period(point.endAt)}${point.partial ? ', partial period' : ''}. ${point.observed + point.unknown ? `${metricDuration(point.active)} with players, ${metricDuration(point.idle)} idle, ${metricDuration(point.unknown)} unknown.` : 'No captured time.'}`;
|
||||
announcement = `${format.period(point.startAt)} to ${format.period(point.endAt)}${point.partial ? ', partial period' : ''}. ${pointSummary(point)}`;
|
||||
}
|
||||
function segmentHeight(point: MetricPoint, value: number) {
|
||||
const denominator = mode === 'percent' ? point.observed + point.unknown : ceiling;
|
||||
const denominator =
|
||||
mode === 'percent'
|
||||
? point.observed + point.unknown
|
||||
: mode === 'average'
|
||||
? ceiling * point.capturedHours
|
||||
: ceiling;
|
||||
return denominator > 0 ? (value / denominator) * 100 : 0;
|
||||
}
|
||||
function averageLabel(point: MetricPoint, value: number) {
|
||||
const average = metricAverageLobbies(value, point.capturedHours);
|
||||
return average === null ? '—' : metricNumber(average);
|
||||
}
|
||||
function pointSummary(point: MetricPoint) {
|
||||
if (point.observed + point.unknown === 0) return 'No captured time';
|
||||
return mode === 'average'
|
||||
? `${averageLabel(point, point.active)} with players · ${averageLabel(point, point.idle)} empty spares · ${averageLabel(point, point.unknown)} unknown on average`
|
||||
: `${metricDuration(point.active)} with players · ${metricDuration(point.idle)} empty spares · ${metricDuration(point.unknown)} unknown`;
|
||||
}
|
||||
function move(direction: number) {
|
||||
const next = points[Math.max(0, Math.min(points.length - 1, selectedIndex + direction))];
|
||||
if (next) selectPeriod(next.at);
|
||||
@@ -98,15 +123,22 @@
|
||||
<section class="panel" aria-labelledby="metrics-trend-title">
|
||||
<div class="heading">
|
||||
<div>
|
||||
<h2 id="metrics-trend-title">Server activity over time</h2>
|
||||
<h2 id="metrics-trend-title">Lobbies running over time</h2>
|
||||
<p>
|
||||
{mode === 'percent'
|
||||
? 'Share of captured server time, including unknown status.'
|
||||
: `Server-hours per ${days === 1 ? 'hour' : '24-hour period'}.`} Partial periods contain
|
||||
less elapsed time.
|
||||
{mode === 'average'
|
||||
? 'Average concurrent lobbies during captured time, including unknown status.'
|
||||
: mode === 'percent'
|
||||
? 'Share of captured lobby time, including unknown status.'
|
||||
: `Lobby-hours per ${days === 1 ? 'hour' : '24-hour period'}.`}
|
||||
Partial periods contain less elapsed time.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mode" role="group" aria-label="Timeline measure">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={mode === 'average'}
|
||||
onclick={() => setMode('average')}>Average lobbies</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={mode === 'percent'}
|
||||
@@ -117,12 +149,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><i class="occupied"></i>With players</span><span><i class="idle"></i>Idle</span><span
|
||||
><i class="unknown"></i>Unknown</span
|
||||
><span class="partial-key">* Partial period</span>
|
||||
<span><i class="occupied"></i>With players</span><span><i class="idle"></i>Empty spare</span
|
||||
><span><i class="unknown"></i>Unknown</span><span class="partial-key">* Partial period</span
|
||||
>
|
||||
</div>
|
||||
<div class="axis-title">
|
||||
{mode === 'percent' ? 'Share of captured server time' : 'Server-hours'}
|
||||
{mode === 'average'
|
||||
? 'Average lobbies running'
|
||||
: mode === 'percent'
|
||||
? 'Share of captured lobby time'
|
||||
: 'Lobby-hours'}
|
||||
</div>
|
||||
<div class="chart-grid">
|
||||
<div class="y-axis" aria-hidden="true">
|
||||
@@ -133,7 +169,7 @@
|
||||
<div
|
||||
class="plot"
|
||||
role="group"
|
||||
aria-label="Server time by period. Select a bar or use the period selector below."
|
||||
aria-label="Lobbies by period. Select a bar or use the period selector below."
|
||||
>
|
||||
<div class="grid-lines" aria-hidden="true">
|
||||
{#each ticks as item}<span style:top={`${(1 - item.fraction) * 100}%`}
|
||||
@@ -147,7 +183,7 @@
|
||||
type="button"
|
||||
tabindex={index === selectedIndex ? 0 : -1}
|
||||
aria-pressed={point.at === selected?.at}
|
||||
aria-label={`${format.period(point.startAt)}${point.partial ? ', partial period' : ''}: ${point.observed + point.unknown ? `${metricDuration(point.active)} with players, ${metricDuration(point.idle)} idle, ${metricDuration(point.unknown)} unknown` : 'No captured time'}`}
|
||||
aria-label={`${format.period(point.startAt)}${point.partial ? ', partial period' : ''}: ${pointSummary(point)}`}
|
||||
title={`${format.period(point.startAt)} – ${format.period(point.endAt)}${point.partial ? ' · partial period' : ''}`}
|
||||
onclick={() => selectPeriod(point.at)}
|
||||
onkeydown={(event) => chartKey(event, index)}
|
||||
@@ -192,8 +228,8 @@
|
||||
: ''}</time
|
||||
><span
|
||||
>{captured
|
||||
? `${metricDuration(selected.active)} with players · ${metricDuration(selected.idle)} idle · ${metricDuration(selected.unknown)} unknown · ${selected.observed ? metricNumber(selected.playerHours) : '—'} player-hours`
|
||||
: 'No captured time · not evidence of idle servers'}</span
|
||||
? `${pointSummary(selected)} · ${selected.observed ? metricNumber(selected.playerHours) : '—'} player-hours`
|
||||
: 'No captured time · not evidence of empty lobbies'}</span
|
||||
>
|
||||
</div>
|
||||
<details class="inspection">
|
||||
@@ -228,7 +264,7 @@
|
||||
</p>
|
||||
{#if captured === 0}<p class="empty">
|
||||
No captured time in this period. Missing observations do not mean the fleet
|
||||
was idle.
|
||||
had empty lobbies.
|
||||
</p>
|
||||
{:else}<dl>
|
||||
<div>
|
||||
@@ -243,7 +279,7 @@
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Idle</dt>
|
||||
<dt>Empty spare</dt>
|
||||
<dd>
|
||||
{metricDuration(selected.idle)}<small
|
||||
>{metricPercent(selected.idle, captured)} of captured time</small
|
||||
@@ -264,9 +300,10 @@
|
||||
</div>
|
||||
</dl>
|
||||
<p class="denominator">
|
||||
{metricDuration(captured)} captured server time · {metricDuration(
|
||||
{metricDuration(captured)} captured lobby time · {metricDuration(
|
||||
selected.observed
|
||||
)} known. Percentage bars include unknown time; the occupancy KPI excludes it.
|
||||
)} known over {metricDuration(selected.capturedHours)} of captured wall time.
|
||||
Averages divide lobby-hours by captured wall time. Collection gaps are excluded.
|
||||
</p>{/if}
|
||||
</div>
|
||||
</details>
|
||||
@@ -283,9 +320,11 @@
|
||||
>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Period</th><th scope="col">With players</th><th scope="col"
|
||||
>Idle</th
|
||||
><th scope="col">Unknown</th><th scope="col">Player-hours</th></tr
|
||||
><th scope="col">Period</th><th scope="col">Average lobbies</th><th
|
||||
scope="col">With players</th
|
||||
><th scope="col">Empty spare</th><th scope="col">Unknown</th><th scope="col"
|
||||
>Player-hours</th
|
||||
></tr
|
||||
></thead
|
||||
>
|
||||
<tbody
|
||||
@@ -296,11 +335,11 @@
|
||||
? ' · Partial'
|
||||
: ''}</small
|
||||
></th
|
||||
><td>{metricDuration(point.active)}</td><td
|
||||
>{metricDuration(point.idle)}</td
|
||||
><td>{metricDuration(point.unknown)}</td><td
|
||||
>{point.observed ? metricNumber(point.playerHours) : '—'}</td
|
||||
></tr
|
||||
><td>{averageLabel(point, point.observed + point.unknown)}</td><td
|
||||
>{metricDuration(point.active)}</td
|
||||
><td>{metricDuration(point.idle)}</td><td
|
||||
>{metricDuration(point.unknown)}</td
|
||||
><td>{point.observed ? metricNumber(point.playerHours) : '—'}</td></tr
|
||||
>{/each}</tbody
|
||||
>
|
||||
</table>
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
import Select from '$ui/components/primitives/Select.svelte';
|
||||
import Field from '$ui/components/primitives/Field.svelte';
|
||||
import type { MetricsData } from '$lib/metrics.js';
|
||||
import { HOSTING_RULES, metricRecommendations } from '$lib/metrics-insights.js';
|
||||
import { POLICY_SIGNAL_RULES, metricRecommendations } from '$lib/metrics-insights.js';
|
||||
let { report }: { report: MetricsData } = $props();
|
||||
const recommendations = $derived(metricRecommendations(report.maps));
|
||||
const highlighted = $derived(
|
||||
recommendations
|
||||
.filter(
|
||||
(item) => !item.tentative && (item.kind === 'expand' || item.kind === 'consolidate')
|
||||
)
|
||||
.filter((item) => !item.tentative && item.kind !== 'watch' && item.kind !== 'reporting')
|
||||
.slice(0, 3)
|
||||
);
|
||||
const remaining = $derived(recommendations.filter((item) => !highlighted.includes(item)));
|
||||
@@ -49,6 +47,7 @@
|
||||
? ' · Limited evidence'
|
||||
: ''}</small
|
||||
>
|
||||
{#if suggestion.policySummary}<small>{suggestion.policySummary}</small>{/if}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{suggestion.title}</strong>
|
||||
@@ -58,17 +57,24 @@
|
||||
<p>{suggestion.reason}</p>
|
||||
<p>{suggestion.outcome}</p>
|
||||
</details>
|
||||
{#if suggestion.href && suggestion.action}
|
||||
<a class="policy-link" href={suggestion.href}>{suggestion.action}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/snippet}
|
||||
<section class="explore" aria-labelledby="metrics-explore-title">
|
||||
<h2 id="metrics-explore-title">Hosting insights</h2>
|
||||
<p>Historical signals for hosting trials. Check the current lineup before making a change.</p>
|
||||
<h2 id="metrics-explore-title">Autoscaling signals</h2>
|
||||
<p>
|
||||
Compare recorded empty-lobby availability with each map’s current policy. Current settings
|
||||
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 highlighted.length}{#each highlighted as suggestion}{@render finding(suggestion)}{/each}
|
||||
{:else}<p class="empty">No hosting trials meet the evidence threshold in this period.</p>{/if}
|
||||
{:else}<p class="empty">No policy changes meet the evidence threshold in this period.</p>{/if}
|
||||
{#if limited}<p class="notice">
|
||||
{limited} of {recommendations.length} maps have limited evidence. Review occupancy reporting,
|
||||
player limits, and observation duration before changing hosting.
|
||||
{limited} of {recommendations.length} maps have limited evidence. Review occupancy reporting
|
||||
and observation duration before changing a policy.
|
||||
</p>{/if}
|
||||
{#if remaining.length}<details class="remaining">
|
||||
<summary>Other findings ({remaining.length})</summary>
|
||||
@@ -104,31 +110,33 @@
|
||||
</p>{/each}
|
||||
</details>{/if}
|
||||
<details class="rules">
|
||||
<summary>How Rules v1 chooses a suggestion</summary>
|
||||
<summary>How policy signals are chosen</summary>
|
||||
<ul>
|
||||
<li>
|
||||
Trial an additional lobby when all tracked lobbies are full for at least {HOSTING_RULES.pressureExpansion *
|
||||
100}% of known-capacity time.
|
||||
Review a policy when no empty lobby was reported for at least {POLICY_SIGNAL_RULES.noEmptyShare *
|
||||
100}% of known map time. When most of that time had a lobby count at or above
|
||||
the current maximum, consider raising the maximum. Otherwise, review the spare
|
||||
target and create time. Equal durations favor reviewing the maximum.
|
||||
</li>
|
||||
<li>
|
||||
When capacity is unavailable, at least {HOSTING_RULES.occupancyExpansion * 100}%
|
||||
occupied known time can suggest a tentative additional-lobby trial. Occupancy does
|
||||
not establish saturation.
|
||||
An empty lobby has a reported player count of zero. When some lobbies report players
|
||||
and the rest are unknown, none reports empty; unknown lobbies may still have been
|
||||
empty. Player limits are not needed for these signals.
|
||||
</li>
|
||||
<li>
|
||||
Review idle hosting when occupied known time is at most {HOSTING_RULES.idleUtilization *
|
||||
100}%, and either measured pressure is below {HOSTING_RULES.quietPressure *
|
||||
100}% or capacity is unavailable.
|
||||
Maps without a current policy suggest adding one. Paused or disabled policies prompt
|
||||
a review before resuming or enabling them.
|
||||
</li>
|
||||
<li>
|
||||
Missing occupancy prompts a reporting check. Other cases suggest watching hosting or
|
||||
checking capacity reporting.
|
||||
All-unknown occupancy prompts a reporting check. Other cases suggest watching
|
||||
empty-lobby availability.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Less than {HOSTING_RULES.minimumEvidenceHours} hour of relevant known time, missing capacity,
|
||||
or substantial unknown status marks advice as tentative. These are trial policies, not validated
|
||||
demand forecasts. Collection gaps are omitted from all rules.
|
||||
Less than {POLICY_SIGNAL_RULES.minimumEvidenceHours} hour of known map availability, or less
|
||||
than {POLICY_SIGNAL_RULES.minimumKnownShare * 100}% known map availability or lobby
|
||||
occupancy, marks a signal as tentative. These signals are advisory, not demand
|
||||
forecasts. Collection gaps are omitted from all rules.
|
||||
</p>
|
||||
</details>
|
||||
</section>
|
||||
@@ -185,6 +193,11 @@
|
||||
border-left: 2px solid var(--state-warning);
|
||||
padding-left: 12px;
|
||||
}
|
||||
.policy-link {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
li {
|
||||
margin: 8px 0;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { MetricsData } from '$lib/metrics.js';
|
||||
import { metricDuration, metricNumber, metricPercent } from '$ui/metrics.js';
|
||||
import { countLabel, plural } from '$ui/plural.js';
|
||||
import Card from '$ui/components/primitives/Card.svelte';
|
||||
import Stat from '$ui/components/primitives/Stat.svelte';
|
||||
let { report }: { report: MetricsData } = $props();
|
||||
const prior = $derived(report.previous);
|
||||
const pressure = $derived(report.pooledPressure);
|
||||
const availability = $derived(report.pooledAvailability);
|
||||
const captured = $derived(report.summary.observed + report.summary.unknown);
|
||||
const comparable = $derived(report.summary.observed > 0 && prior.summary.observed > 0);
|
||||
const occupancyDelta = $derived(
|
||||
comparable
|
||||
? 100 *
|
||||
(report.summary.active / report.summary.observed -
|
||||
prior.summary.active / prior.summary.observed)
|
||||
: null
|
||||
);
|
||||
const playerDelta = $derived(
|
||||
comparable && prior.summary.playerHours > 0
|
||||
? 100 * (report.summary.playerHours / prior.summary.playerHours - 1)
|
||||
@@ -32,19 +27,21 @@
|
||||
<div class="summary">
|
||||
<Card
|
||||
><Stat
|
||||
label="Time with players"
|
||||
value={metricPercent(report.summary.active, report.summary.observed)}
|
||||
label="Lobbies running"
|
||||
value={report.running.average === null
|
||||
? '—'
|
||||
: `${metricNumber(report.running.average)} avg`}
|
||||
size="lg"
|
||||
tween={false}
|
||||
/>
|
||||
<p>
|
||||
{occupancyDelta === null
|
||||
? 'Comparison unavailable'
|
||||
: `${signed(occupancyDelta, ' percentage points')} vs ${previousLabel}`}
|
||||
{report.running.average === null
|
||||
? 'Peak unavailable'
|
||||
: `Peak ${countLabel(report.running.peak, 'lobby', 'lobbies')} at once`}
|
||||
</p>
|
||||
<small
|
||||
>{metricDuration(report.summary.active)} of {metricDuration(report.summary.observed)} observed
|
||||
server time</small
|
||||
>{metricDuration(report.summary.idle)} of {metricDuration(captured)} captured lobby-hours
|
||||
were empty spares ({metricPercent(report.summary.idle, captured)})</small
|
||||
>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -68,15 +65,15 @@
|
||||
</Card>
|
||||
<Card
|
||||
><Stat
|
||||
label="Time all lobbies were full"
|
||||
value={metricPercent(pressure.fullMapHours, pressure.knownMapHours)}
|
||||
label="Time with no empty lobby"
|
||||
value={metricPercent(availability.noEmptyMapHours, availability.knownMapHours)}
|
||||
size="lg"
|
||||
tween={false}
|
||||
/>
|
||||
<p>
|
||||
{pressure.knownMapHours > 0
|
||||
? `${pressure.coveredMaps} of ${pressure.totalMaps} maps have capacity data`
|
||||
: 'Capacity data unavailable'}
|
||||
{availability.knownMapHours > 0
|
||||
? `${availability.coveredMaps} of ${countLabel(availability.totalMaps, 'map')} ${plural(availability.totalMaps, 'has', 'have')} availability data`
|
||||
: 'Lobby availability unavailable'}
|
||||
</p>
|
||||
<small>Combined map history · each map measured separately</small>
|
||||
</Card>
|
||||
@@ -84,13 +81,9 @@
|
||||
{#if prior.partialHistory}<p class="notice">
|
||||
Partial previous history. Comparisons use available observations without extrapolation.
|
||||
</p>{/if}
|
||||
{#if report.maps.some((map) => map.capacityObserved <= 0)}<p class="notice">
|
||||
{report.maps.filter((map) => map.capacityObserved <= 0).length} of {report.maps.length} maps lack
|
||||
capacity data. Player limits are needed to measure full lobbies; occupancy can still be recorded.
|
||||
</p>{/if}
|
||||
{#if report.summary.unknown > 0}<p class="quality">
|
||||
{metricDuration(report.summary.unknown)} of captured status is unknown and excluded from “Time
|
||||
with players.” Collection gaps are separate.
|
||||
{metricDuration(report.summary.unknown)} of captured lobby time had unknown status. Unknown lobbies
|
||||
are included in running counts and excluded from empty spares. Collection gaps are separate.
|
||||
</p>{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{ key: 'map', label: 'Map' },
|
||||
{ key: 'playerHours', label: 'Player-hours' },
|
||||
{ key: 'active', label: 'With players' },
|
||||
{ key: 'full', label: 'All lobbies full' },
|
||||
{ key: 'noEmpty', label: 'No empty lobby' },
|
||||
{ key: 'details', label: 'Details' }
|
||||
] as const;
|
||||
const serverColumns = [
|
||||
@@ -109,7 +109,8 @@
|
||||
}
|
||||
function mapValue(map: MapMetrics, key: Exclude<MapSort, 'map'>): number | null {
|
||||
if (key === 'details') return null;
|
||||
if (key === 'full') return map.capacityObserved ? map.full / map.capacityObserved : null;
|
||||
if (key === 'noEmpty')
|
||||
return map.availabilityObserved ? map.noEmpty / map.availabilityObserved : null;
|
||||
return (key === 'playerHours' || key === 'active') && !map.observed ? null : map[key];
|
||||
}
|
||||
function serverValue(server: ServerMetrics, key: Exclude<ServerSort, 'server'>): number | null {
|
||||
@@ -159,7 +160,7 @@
|
||||
|
||||
<section class="panel" aria-label={view === 'maps' ? 'Map metrics' : 'Instance metrics'}>
|
||||
<div class="heading">
|
||||
<h2>{view === 'maps' ? 'Map play time and capacity' : 'Instance utilization'}</h2>
|
||||
<h2>{view === 'maps' ? 'Map play time and lobby availability' : 'Instance utilization'}</h2>
|
||||
<div class="search">
|
||||
<Field label={view === 'maps' ? 'Find a map' : 'Find an instance'}
|
||||
><Input
|
||||
@@ -225,8 +226,9 @@
|
||||
<th scope="row"
|
||||
>{map.map}<small
|
||||
>{gameName(map.game)}{map.observed < 1 ||
|
||||
map.capacityObserved < 1 ||
|
||||
map.unknown > map.observed / 4
|
||||
map.availabilityObserved < 1 ||
|
||||
map.unknown > map.observed / 4 ||
|
||||
map.availabilityUnknown > map.availabilityObserved / 4
|
||||
? ' · Limited evidence'
|
||||
: ''}</small
|
||||
></th
|
||||
@@ -240,23 +242,25 @@
|
||||
</div></td
|
||||
>
|
||||
<td>{map.observed ? metricDuration(map.active) : '—'}</td>
|
||||
<td>{metricPercent(map.full, map.capacityObserved)}</td>
|
||||
<td>{metricPercent(map.noEmpty, map.availabilityObserved)}</td>
|
||||
<td
|
||||
><details>
|
||||
<summary
|
||||
><span class="sr-only">{map.map} </span>Details</summary
|
||||
>
|
||||
<dl>
|
||||
<dt>Observed instance time</dt>
|
||||
<dt>Observed lobby time</dt>
|
||||
<dd>{metricDuration(map.observed)}</dd>
|
||||
<dt>Unknown status</dt>
|
||||
<dd>{metricDuration(map.unknown)}</dd>
|
||||
<dt>All lobbies full</dt>
|
||||
<dt>No empty lobby</dt>
|
||||
<dd>
|
||||
{map.capacityObserved
|
||||
? `${metricDuration(map.full)} of ${metricDuration(map.capacityObserved)} known map time`
|
||||
: 'Capacity data unavailable'}
|
||||
{map.availabilityObserved
|
||||
? `${metricDuration(map.noEmpty)} of ${metricDuration(map.availabilityObserved)} known map time`
|
||||
: 'Lobby availability unavailable'}
|
||||
</dd>
|
||||
<dt>Unknown map availability</dt>
|
||||
<dd>{metricDuration(map.availabilityUnknown)}</dd>
|
||||
</dl>
|
||||
</details></td
|
||||
>
|
||||
@@ -289,7 +293,7 @@
|
||||
<dd><code>{server.server}</code></dd>
|
||||
<dt>Time with players</dt>
|
||||
<dd>{metricDuration(server.active)}</dd>
|
||||
<dt>Idle</dt>
|
||||
<dt>Empty spare</dt>
|
||||
<dd>
|
||||
{metricPercent(server.idle, server.observed)} · {metricDuration(
|
||||
server.idle
|
||||
@@ -338,7 +342,7 @@
|
||||
<summary>About these metrics</summary>
|
||||
<p>
|
||||
{view === 'maps'
|
||||
? 'All lobbies full uses map time when every tracked lobby has known occupancy and capacity. It does not measure failed joins or unmet demand. A dash means unavailable, not zero.'
|
||||
? 'No empty lobby measures map time when at least one tracked lobby has known occupancy and none report zero players. If every lobby is unknown, map availability is unknown. Player limits are not required. This does not measure failed joins or unmet demand. A dash means unavailable, not zero.'
|
||||
: 'Time with players excludes unknown status and collection gaps. Rows group historical observations by name and may span multiple registrations. Links open the current instance with that name.'}
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user