Files
electric-cherry/src/lib/ui/components/AfkReserveCard.svelte
T
2026-10-17 17:52:03 -04:00

207 lines
7.8 KiB
Svelte
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.
<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';
import Button from './primitives/Button.svelte';
let { account, guid }: { account: ReturnType<typeof afkProfile>; guid?: string } = $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">
<div class="heading">
<h2 id="afk-title">AFK reserve</h2>
{#if guid}<Button size="sm" href={`/afk?view=ledger&player=${encodeURIComponent(guid)}`}
>View AFK history</Button
>{/if}
</div>
<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);
}
.heading {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
}
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>