This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import type { afkProfile } from '$lib/server/fleet/afk.js';
|
||||
import { formatDuration } from '$ui/duration.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { serverTitle } from '$ui/server-labels.js';
|
||||
|
||||
let { account }: { account: ReturnType<typeof afkProfile> } = $props();
|
||||
const duration = (seconds: number) => formatDuration(seconds * 1000);
|
||||
const change = (seconds: number) =>
|
||||
`${seconds > 0 ? '+' : seconds < 0 ? '−' : ''}${duration(Math.abs(seconds))}`;
|
||||
const label = (value: string) => value.replaceAll('_', ' ');
|
||||
</script>
|
||||
|
||||
<section aria-labelledby="afk-title">
|
||||
<h2 id="afk-title">AFK reserve</h2>
|
||||
<p>
|
||||
<strong>{duration(account.freeSeconds)} free</strong> and
|
||||
<strong>{duration(account.savedSeconds)} saved</strong>, shared across Black Ops I and II.
|
||||
Saved capacity: {duration(account.capacitySeconds)}.
|
||||
</p>
|
||||
{#if account.freeRenewsAt}
|
||||
<p>
|
||||
Free time next replenishes {$timeFormat.dateTime(account.freeRenewsAt)}. Unused free
|
||||
time does not accumulate.
|
||||
</p>
|
||||
{/if}
|
||||
{#if account.reservedSeconds > 0}
|
||||
<p>
|
||||
{duration(account.reservedSeconds)} is reserved for a session and unavailable to other servers.
|
||||
</p>
|
||||
{/if}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Recorded active play</dt>
|
||||
<dd>{duration(account.activeSeconds)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Confirmed AFK time</dt>
|
||||
<dd>{duration(account.confirmedAfkSeconds)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Manual returns</dt>
|
||||
<dd>{account.manualReturns}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Expirations</dt>
|
||||
<dd>{account.expiredSessions}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Purchases</dt>
|
||||
<dd>{account.purchaseCount} ({duration(account.purchasedSeconds)})</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{#if account.unresolvedSessions > 0}
|
||||
<p>
|
||||
{account.unresolvedSessions} session(s) have an unconfirmed duration. Their time is excluded
|
||||
from confirmed AFK usage.
|
||||
</p>
|
||||
{/if}
|
||||
{#if account.entries.length}
|
||||
<details>
|
||||
<summary>Recent time ledger</summary>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<caption>Recent AFK grants, reservations and consumption</caption>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Time / event</th><th scope="col">Free change</th><th
|
||||
scope="col">Saved change</th
|
||||
><th scope="col">Balances after</th><th scope="col">Server / match</th
|
||||
></tr
|
||||
></thead
|
||||
>
|
||||
<tbody>
|
||||
{#each account.entries as entry (entry.id)}
|
||||
<tr>
|
||||
<td
|
||||
>{$timeFormat.dateTime(entry.eventAt)}<small
|
||||
>{label(entry.kind)}</small
|
||||
><small>{entry.operationId}</small></td
|
||||
>
|
||||
<td>{change(entry.freeDelta)}</td>
|
||||
<td>{change(entry.savedDelta)}</td>
|
||||
<td
|
||||
>{duration(entry.freeSeconds)} free<small
|
||||
>{duration(entry.savedSeconds)} saved</small
|
||||
></td
|
||||
>
|
||||
<td
|
||||
>{serverTitle(entry.server)}<small
|
||||
>{#if entry.matchAvailable}<a
|
||||
href={`/matches/${entry.matchId}`}
|
||||
>Match #{entry.matchId}</a
|
||||
>{:else if entry.matchId}Match #{entry.matchId} (history expired){:else}—{/if}</small
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<p>No AFK activity recorded yet.</p>
|
||||
{/if}
|
||||
{#if account.sessions.length}
|
||||
<details>
|
||||
<summary>Recent AFK sessions</summary>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<caption>Accepted AFK transitions</caption>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Started</th><th scope="col">Server</th><th scope="col"
|
||||
>Duration</th
|
||||
><th scope="col">Outcome</th></tr
|
||||
></thead
|
||||
>
|
||||
<tbody>
|
||||
{#each account.sessions as session (session.sessionId)}
|
||||
<tr>
|
||||
<td
|
||||
>{session.startedAt
|
||||
? $timeFormat.dateTime(session.startedAt)
|
||||
: 'Start unconfirmed'}</td
|
||||
>
|
||||
<td>{serverTitle(session.server)}</td>
|
||||
<td
|
||||
>{session.consumedSeconds === null
|
||||
? 'Unconfirmed'
|
||||
: duration(session.consumedSeconds)}
|
||||
{#if session.state === 'corrected' && session.chargedSeconds !== null}
|
||||
<small
|
||||
>Corrected charge: {duration(
|
||||
session.chargedSeconds
|
||||
)}</small
|
||||
>
|
||||
{/if}</td
|
||||
>
|
||||
<td
|
||||
>{label(
|
||||
session.endReason ?? session.state
|
||||
)}{#if session.endedAt}<small
|
||||
>{$timeFormat.dateTime(session.endedAt)}</small
|
||||
>{/if}</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 var(--space-2);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
dl {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
dt,
|
||||
small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
dd {
|
||||
margin: var(--space-1) 0 0;
|
||||
}
|
||||
small {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
details {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
a {
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user