add live statistics
All checks were successful
deploy / build-and-deploy (push) Successful in 22s

This commit is contained in:
2025-11-11 01:53:56 +01:00
parent dfc1425c6b
commit 5ce2db9040
26 changed files with 1168 additions and 817 deletions

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addAdmin, fetchAdmins } from '@app/admin/admins/admins.ts';
import { Permissions } from '@util/permissions.ts';
@@ -15,7 +14,7 @@
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neuer Admin</span>
</button>
</div>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { fetchBlockedUsers, addBlockedUser } from '@app/admin/blockedUsers/blockedUsers.ts';
@@ -14,7 +13,7 @@
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neuer blockierter Nutzer</span>
</button>
</div>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts';
@@ -14,7 +13,7 @@
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neue direkte Einladung</span>
</button>
</div>

View File

@@ -5,7 +5,6 @@
import Select from '@components/input/Select.svelte';
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Icon from '@iconify/svelte';
import UserSearch from '@components/admin/search/UserSearch.svelte';
// html bindings
@@ -90,7 +89,9 @@
>
<div class="absolute right-2 top-2">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost"><Icon icon="heroicons:share" /></div>
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost">
<span class="iconify iconify-[heroicons--share]"></span>
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul tabindex="0" class="menu dropdown-content bg-base-100 rounded-box z-1 p-2 shadow-sm w-max">
<li><button onclick={() => onCopyPublicLink(report?.urlHash)}>Öffentlichen Report Link kopieren</button></li>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import Input from '@components/input/Input.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addReport, fetchReports } from '@app/admin/reports/reports.ts';
@@ -27,7 +26,7 @@
</fieldset>
<div class="divider my-1"></div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neuer Report</span>
</button>
</div>

View File

@@ -1,5 +1,4 @@
<script>
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addStrikeReason, fetchStrikeReasons } from '@app/admin/strikeReasons/strikeReasons.js';
@@ -14,7 +13,7 @@
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neuer Strikegrund</span>
</button>
</div>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import Input from '@components/input/Input.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addUser, fetchUsers } from '@app/admin/users/users.ts';
@@ -22,7 +21,7 @@
</fieldset>
<div class="divider my-1"></div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
<span>Neuer Nutzer</span>
</button>
</div>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { STATISTICS_INTERVAL } from 'astro:env/server';
import { statistics } from '@app/website/index/liveStats.ts';
function transformPlaytimeMinutes(playtimeMinutes: number) {
return playtimeMinutes < 60 * 24
? `${Math.floor(playtimeMinutes / 60)} Stunden`
: `${Math.floor(playtimeMinutes / 60 / 24)} Tage`;
}
function transformMobKills(mobKills: number) {
return mobKills < 1000 ? `${mobKills}` : `${Math.floor(mobKills / 1000)}`;
}
</script>
<div class="flex flex-col items-center">
<div class="flex flex-row items-start mb-6">
<div class="tooltip" data-tip="* Die Statistiken werden alle {STATISTICS_INTERVAL / 60} Minuten aktualisiert">
<h3 class="text-2xl">Live<span class="font-geist text-[18px]">*</span> Statistiken</h3>
</div>
<div class="inline-grid *:[grid-area:1/1] mt-1 ml-0.5">
<div class="status status-info animate-ping"></div>
<div class="status status-info"></div>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-4">
<div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow h-min xl:h-[initial]">
<div class="stat">
<div class="stat-figure">
<span class="iconify iconify-[heroicons--clock-solid] size-[1.5em]"></span>
</div>
<div class="stat-title">Gesamtspielzeit</div>
<div class="stat-value">
{statistics != null ? transformPlaytimeMinutes(statistics.playtimeMinutes) : 'n/a'}
</div>
<div class="stat-desc">&#8203;</div>
</div>
</div>
<div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow">
<div class="stat">
<div class="stat-figure">
<span class="iconify iconify-[local--crosshairs] size-[1.5em]"></span>
</div>
<div class="stat-title">Getötete Monster</div>
<div class="stat-value">{statistics != null ? transformMobKills(statistics.mobKills) : 'n/a'}</div>
<div class="stat-desc">&#8203;</div>
</div>
<div class="stat">
<div class="stat-figure">
<span class="iconify iconify-[local--skull] size-[1.5em]"></span>
</div>
<div class="stat-title">Spieler Tode</div>
<div class="stat-value">{statistics?.playerDeaths ?? 'n/a'}</div>
<div class="stat-desc">
<span class="underline">{statistics?.playerKills ?? 'n/a'}</span> davon durch andere Spieler
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,67 @@
import { STATISTICS_ENDPOINT, STATISTICS_INTERVAL } from 'astro:env/server';
import { logger } from '@util/log.ts';
export let statistics: Awaited<ReturnType<typeof fetchStatistics>> | null = null;
if (STATISTICS_ENDPOINT) {
statistics = await fetchStatistics().catch((_) => null);
setInterval(
() =>
fetchStatistics()
.catch((_) => null)
.then((val) => (statistics = val)),
STATISTICS_INTERVAL * 60 * 1000
);
}
async function fetchStatistics() {
const response = (await fetch(STATISTICS_ENDPOINT!, {
method: 'POST',
body: JSON.stringify({
categories: [{ name: 'PLAY_ONE_MINUTE' }, { name: 'PLAYER_KILLS' }, { name: 'DEATHS' }, { name: 'MOB_KILLS' }]
})
}).catch((e) => {
logger.warn(
{
error: e
},
'could not fetch statistics'
);
throw e;
})) as Response;
type StatisticName = 'PLAY_ONE_MINUTE' | 'PLAYER_KILLS' | 'DEATHS' | 'MOB_KILLS';
type ResponseData = {
response: {
playerStatistics: {
playerName: string;
statistics: { name: StatisticName; value: number }[];
}[];
};
};
const responseData: ResponseData = await response.json();
let playtimeMinutes = 0;
let playerKills = 0;
let playerDeaths = 0;
let mobKills = 0;
for (const player of responseData.response.playerStatistics) {
for (const statistic of player.statistics) {
switch (statistic.name) {
case 'PLAY_ONE_MINUTE':
playtimeMinutes += statistic.value / 20 / 60;
break;
case 'PLAYER_KILLS':
playerKills += statistic.value;
break;
case 'DEATHS':
playerDeaths += statistic.value;
break;
case 'MOB_KILLS':
mobKills += statistic.value;
break;
}
}
}
return { playtimeMinutes, playerKills, playerDeaths, mobKills };
}