Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { TELEMETRY_REQUEST_RETENTION, type TelemetryRequestLogEntry } from '$lib/telemetry-diagnostics.js';
|
||||
import {
|
||||
TELEMETRY_REQUEST_RETENTION,
|
||||
type TelemetryRequestLogEntry
|
||||
} from '$lib/telemetry-diagnostics.js';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { serverTitle } from '$ui/server-labels.js';
|
||||
import Card from './primitives/Card.svelte';
|
||||
@@ -12,7 +15,9 @@
|
||||
|
||||
let { server }: { server: string } = $props();
|
||||
let requests = $state<TelemetryRequestLogEntry[]>([]);
|
||||
let retention = $state<{ maxPerServer: number; maxAgeDays: number }>({ ...TELEMETRY_REQUEST_RETENTION });
|
||||
let retention = $state<{ maxPerServer: number; maxAgeDays: number }>({
|
||||
...TELEMETRY_REQUEST_RETENTION
|
||||
});
|
||||
let refreshing = $state(false);
|
||||
let loaded = $state(false);
|
||||
let error = $state('');
|
||||
@@ -31,13 +36,18 @@
|
||||
controller = requestController;
|
||||
refreshing = true;
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${encodeURIComponent(currentServer)}/telemetry-requests`, {
|
||||
cache: 'no-store',
|
||||
signal: requestController.signal
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/servers/${encodeURIComponent(currentServer)}/telemetry-requests`,
|
||||
{
|
||||
cache: 'no-store',
|
||||
signal: requestController.signal
|
||||
}
|
||||
);
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok || !body?.ok || !Array.isArray(body.requests)) {
|
||||
throw new Error(body?.error || `Could not load telemetry requests (HTTP ${response.status}).`);
|
||||
throw new Error(
|
||||
body?.error || `Could not load telemetry requests (HTTP ${response.status}).`
|
||||
);
|
||||
}
|
||||
if (!active || server !== currentServer || requestController.signal.aborted) return;
|
||||
requests = body.requests;
|
||||
@@ -47,7 +57,8 @@
|
||||
refreshedAt = Date.now();
|
||||
} catch (cause) {
|
||||
if (active && server === currentServer && !requestController.signal.aborted) {
|
||||
error = cause instanceof Error ? cause.message : 'Could not load telemetry requests.';
|
||||
error =
|
||||
cause instanceof Error ? cause.message : 'Could not load telemetry requests.';
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
@@ -78,13 +89,21 @@
|
||||
<h2>Telemetry API requests</h2>
|
||||
<p class="server-name">{serverTitle(server)}</p>
|
||||
</div>
|
||||
<Freshness updatedAt={refreshedAt} error={error ? 'Telemetry request updates interrupted.' : ''} intervalLabel="Updates every 5 seconds" onRetry={() => void refresh()} retryDisabled={refreshing} />
|
||||
<Freshness
|
||||
updatedAt={refreshedAt}
|
||||
error={error ? 'Telemetry request updates interrupted.' : ''}
|
||||
intervalLabel="Updates every 5 seconds"
|
||||
onRetry={() => void refresh()}
|
||||
retryDisabled={refreshing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="load-error" role="alert">
|
||||
Couldn’t load telemetry requests. {#if loaded} Previously loaded requests are shown below.{/if}
|
||||
</p><details class="payload"><summary>Technical details</summary>{error}</details>
|
||||
Couldn’t load telemetry requests. {#if loaded}
|
||||
Previously loaded requests are shown below.{/if}
|
||||
</p>
|
||||
<details class="payload"><summary>Technical details</summary>{error}</details>
|
||||
{/if}
|
||||
|
||||
{#if !loaded && !error}
|
||||
@@ -100,7 +119,13 @@
|
||||
<li class="request">
|
||||
<div class="request-head">
|
||||
<div class="request-result">
|
||||
<Badge variant={request.status >= 500 ? 'danger' : request.status >= 400 ? 'warning' : 'online'}>
|
||||
<Badge
|
||||
variant={request.status >= 500
|
||||
? 'danger'
|
||||
: request.status >= 400
|
||||
? 'warning'
|
||||
: 'online'}
|
||||
>
|
||||
{outcome(request.status)} · HTTP {request.status}
|
||||
</Badge>
|
||||
<code class="event">{request.event ?? 'Event not provided'}</code>
|
||||
@@ -108,16 +133,32 @@
|
||||
<span class="received"><RelativeTime value={request.receivedAt} /></span>
|
||||
</div>
|
||||
<dl class="request-meta">
|
||||
<div><dt>Map</dt><dd><code>{request.map ?? 'Not provided'}</code></dd></div>
|
||||
<div><dt>Round</dt><dd>{request.round ?? 'Not provided'}</dd></div>
|
||||
<div><dt>Players</dt><dd>{request.playerCount ?? 'Not provided'}</dd></div>
|
||||
<div>
|
||||
<dt>Map</dt>
|
||||
<dd><code>{request.map ?? 'Not provided'}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Round</dt>
|
||||
<dd>{request.round ?? 'Not provided'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Players</dt>
|
||||
<dd>{request.playerCount ?? 'Not provided'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{#if request.error}<p class="request-error">{request.error}</p>{/if}
|
||||
<details class="payload">
|
||||
<summary>Request payload{#if request.payloadTruncated} (truncated){/if}</summary>
|
||||
<p class="content-type">Content type: <code>{request.contentType ?? 'Not provided'}</code></p>
|
||||
<summary
|
||||
>Request payload{#if request.payloadTruncated}
|
||||
(truncated){/if}</summary
|
||||
>
|
||||
<p class="content-type">
|
||||
Content type: <code>{request.contentType ?? 'Not provided'}</code>
|
||||
</p>
|
||||
<pre>{request.payload}</pre>
|
||||
{#if request.payloadTruncated}<p class="truncation">Only the beginning of this payload was retained.</p>{/if}
|
||||
{#if request.payloadTruncated}<p class="truncation">
|
||||
Only the beginning of this payload was retained.
|
||||
</p>{/if}
|
||||
</details>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -125,59 +166,155 @@
|
||||
<ListFooter total={requests.length} bind:limit label="requests" />
|
||||
{/if}
|
||||
|
||||
<details class="request-help"><summary>About request logging</summary> <p class="intro">
|
||||
Incoming <code>POST /api/telemetry</code> requests with the exact server name <code>{server}</code>.
|
||||
Fresh game telemetry supplies player counts when available; RCON provides a fallback. The scoreboard uses telemetry.
|
||||
</p>
|
||||
<p class="retention">
|
||||
Newest first · {retention.maxPerServer ? `up to ${retention.maxPerServer} requests per server` : 'no per-server retention cap'} · {retention.maxAgeDays ? `last ${retention.maxAgeDays} days` : 'no age expiry'}. Fleet-wide limits also apply.
|
||||
</p>
|
||||
<details class="request-help">
|
||||
<summary>About request logging</summary>
|
||||
<p class="intro">
|
||||
Incoming <code>POST /api/telemetry</code> requests with the exact server name
|
||||
<code>{server}</code>. Fresh game telemetry supplies player counts when available; RCON
|
||||
provides a fallback. The scoreboard uses telemetry.
|
||||
</p>
|
||||
<p class="retention">
|
||||
Newest first · {retention.maxPerServer
|
||||
? `up to ${retention.maxPerServer} requests per server`
|
||||
: 'no per-server retention cap'} · {retention.maxAgeDays
|
||||
? `last ${retention.maxAgeDays} days`
|
||||
: 'no age expiry'}. Fleet-wide limits also apply.
|
||||
</p>
|
||||
|
||||
<p class="attribution">
|
||||
Requests can be linked here only when readable JSON contains a matching top-level <code>server</code> field.
|
||||
Calls that never reach Electric Cherry, malformed JSON, and different server names will not appear here.
|
||||
</p></details>
|
||||
<p class="attribution">
|
||||
Requests can be linked here only when readable JSON contains a matching top-level <code
|
||||
>server</code
|
||||
> field. Calls that never reach Electric Cherry, malformed JSON, and different server names
|
||||
will not appear here.
|
||||
</p>
|
||||
</details>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.request-help { margin-top: 16px; font-size: var(--text-xs); }
|
||||
.panel-head, .request-head, .request-result {
|
||||
.request-help {
|
||||
margin-top: 16px;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.panel-head,
|
||||
.request-head,
|
||||
.request-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.panel-head { margin-bottom: var(--space-4); }
|
||||
h2 { margin: 0; font-size: var(--text-md); font-weight: var(--weight-semibold); }
|
||||
p { margin: 0; }
|
||||
code, pre { font-family: var(--font-mono); }
|
||||
.server-name { margin-top: var(--space-2); overflow-wrap: anywhere; color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
.intro { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.6; }
|
||||
.retention, .attribution, .content-type, .truncation {
|
||||
.panel-head {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.server-name {
|
||||
margin-top: var(--space-2);
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.intro {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.retention,
|
||||
.attribution,
|
||||
.content-type,
|
||||
.truncation {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.retention { margin-top: var(--space-2); }
|
||||
.attribution { margin-top: var(--space-4); }
|
||||
.request-list { list-style: none; padding: 0; margin: var(--space-5) 0 0; }
|
||||
.request { padding: var(--space-4) 0; border-top: 1px solid var(--border-subtle); }
|
||||
.request-result { justify-content: flex-start; min-width: 0; }
|
||||
.event { font-size: var(--text-sm); overflow-wrap: anywhere; }
|
||||
.received { color: var(--text-tertiary); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
||||
.request-meta { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-6); margin: var(--space-3) 0; font-size: var(--text-xs); }
|
||||
.request-meta div { display: flex; gap: var(--space-2); min-width: 0; }
|
||||
dt { color: var(--text-tertiary); }
|
||||
dd { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; }
|
||||
.load-error, .request-error { color: var(--state-danger); font-size: var(--text-sm); overflow-wrap: anywhere; }
|
||||
.load-error { margin-top: var(--space-4); }
|
||||
.request-error { margin-bottom: var(--space-3); }
|
||||
.payload { font-size: var(--text-xs); }
|
||||
summary { color: var(--text-secondary); cursor: pointer; width: fit-content; }
|
||||
summary:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
|
||||
.content-type { margin-top: var(--space-3); }
|
||||
.retention {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.attribution {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
.request-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: var(--space-5) 0 0;
|
||||
}
|
||||
.request {
|
||||
padding: var(--space-4) 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
.request-result {
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
.event {
|
||||
font-size: var(--text-sm);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.received {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.request-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3) var(--space-6);
|
||||
margin: var(--space-3) 0;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.request-meta div {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
dt {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.load-error,
|
||||
.request-error {
|
||||
color: var(--state-danger);
|
||||
font-size: var(--text-sm);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.load-error {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
.request-error {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.payload {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
summary {
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
}
|
||||
summary:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.content-type {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
pre {
|
||||
margin: var(--space-2) 0 0;
|
||||
padding: var(--space-3);
|
||||
@@ -191,5 +328,7 @@
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.truncation { margin-top: var(--space-2); }
|
||||
.truncation {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user