fetch statistics from the server

This commit is contained in:
Fran Jurmanović
2025-05-30 13:31:48 +02:00
parent 9d388816c3
commit d5f7df3ed0
5 changed files with 85 additions and 158 deletions

View File

@@ -9,7 +9,8 @@ import {
type EventConfig, type EventConfig,
type EventRules, type EventRules,
type ServerSettings, type ServerSettings,
type StateHistory type StateHistory,
type StateHistoryStats
} from '$models/config'; } from '$models/config';
import type { Server } from '$models/server'; import type { Server } from '$models/server';
import type { RequestEvent } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit';
@@ -49,6 +50,18 @@ export const getStateHistory = async (
); );
}; };
export const getStateHistoryStats = async (
event: RequestEvent,
serverId: string,
startDate: string,
endDate: string
): Promise<StateHistoryStats> => {
return fetchAPIEvent(
event,
`/server/${serverId}/state-history/statistics?start_date=${startDate}&end_date=${endDate}`
);
};
export const getEventFile = async (event: RequestEvent, serverId: string): Promise<EventConfig> => { export const getEventFile = async (event: RequestEvent, serverId: string): Promise<EventConfig> => {
return fetchAPIEvent(event, `/server/${serverId}/config/${configFile.event}`); return fetchAPIEvent(event, `/server/${serverId}/config/${configFile.event}`);
}; };

View File

@@ -4,16 +4,16 @@
import { compareAsc, isSameDay } from 'date-fns'; import { compareAsc, isSameDay } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz'; import { formatInTimeZone } from 'date-fns-tz';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import type { StateHistory } from '$models/config'; import type { StateHistoryStats } from '$models/config';
import { flatMap } from 'lodash-es'; import { flatMap } from 'lodash-es';
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
interface Props { interface Props {
// Props // Props
stateHistory?: Array<StateHistory>; statistics: StateHistoryStats;
} }
let { stateHistory = [] }: Props = $props(); let { statistics }: Props = $props();
// Chart instances // Chart instances
let playerCountChart: Chart | null = null; let playerCountChart: Chart | null = null;
@@ -25,21 +25,8 @@
let sessionTypeCanvas: HTMLCanvasElement | undefined = $state(); let sessionTypeCanvas: HTMLCanvasElement | undefined = $state();
let dailyActivityCanvas: HTMLCanvasElement | undefined = $state(); let dailyActivityCanvas: HTMLCanvasElement | undefined = $state();
let totalSessions = $state(0);
let averagePlayerCount = $state(0);
let peakPlayerCount = $state(0);
let totalPlaytime = $state(0);
let dailyActivityData = $state<{
labels: string[];
data: {
count: number;
sessions: StateHistory[];
}[];
} | null>(null);
// Initialize date range (last 30 days by default) // Initialize date range (last 30 days by default)
onMount(() => { onMount(() => {
processData();
createCharts(); createCharts();
}); });
@@ -50,43 +37,19 @@
if (dailyActivityChart) dailyActivityChart.destroy(); if (dailyActivityChart) dailyActivityChart.destroy();
}); });
function processData() {
calculateSummaryStats();
}
function calculateSummaryStats() {
totalSessions = stateHistory.length;
if (stateHistory.length > 0) {
const playerCounts = stateHistory.map((item) => item.playerCount);
averagePlayerCount = Math.round(
playerCounts.reduce((a, b) => a + b, 0) / playerCounts.length
);
peakPlayerCount = Math.max(...playerCounts);
totalPlaytime = stateHistory.reduce(
(total, session) => total + session.sessionDurationMinutes,
0
);
} else {
averagePlayerCount = 0;
peakPlayerCount = 0;
totalPlaytime = 0;
}
}
function createCharts() { function createCharts() {
if (!playerCountCanvas || !sessionTypeCanvas || !dailyActivityCanvas) return; if (!statistics || !playerCountCanvas || !sessionTypeCanvas || !dailyActivityCanvas) return;
// Player Count Over Time Chart
const playerCountData = preparePlayerCountData();
playerCountChart = new Chart(playerCountCanvas, { playerCountChart = new Chart(playerCountCanvas, {
type: 'line', type: 'line',
data: { data: {
labels: playerCountData.map(({ x }) => x), labels: statistics.playerCountOverTime.map(({ timestamp }) =>
formatDate(timestamp, 'MMM dd kk:mm')
),
datasets: [ datasets: [
{ {
label: 'Player Count', label: 'Player Count',
data: playerCountData.map(({ y }) => y), data: statistics.playerCountOverTime.map(({ count }) => count),
borderColor: '#10b981', borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)', backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true, fill: true,
@@ -107,15 +70,13 @@
} }
}); });
// Session Types Pie Chart
const sessionTypeData = prepareSessionTypeData();
sessionTypeChart = new Chart(sessionTypeCanvas, { sessionTypeChart = new Chart(sessionTypeCanvas, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: sessionTypeData.labels, labels: statistics.sessionTypes.map(({ name }) => name),
datasets: [ datasets: [
{ {
data: sessionTypeData.data, data: statistics.sessionTypes.map(({ count }) => count),
backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'] backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
} }
] ]
@@ -135,16 +96,14 @@
} }
}); });
// Daily Activity Bar Chart
dailyActivityData = prepareDailyActivityData();
dailyActivityChart = new Chart(dailyActivityCanvas, { dailyActivityChart = new Chart(dailyActivityCanvas, {
type: 'bar', type: 'bar',
data: { data: {
labels: dailyActivityData.labels, labels: statistics.dailyActivity.map(({ date }) => date),
datasets: [ datasets: [
{ {
label: 'Sessions', label: 'Sessions',
data: dailyActivityData.data.map(({ count }) => count), data: statistics.dailyActivity.map(({ sessionsCount }) => sessionsCount),
backgroundColor: '#10b981', backgroundColor: '#10b981',
borderColor: '#059669', borderColor: '#059669',
borderWidth: 1 borderWidth: 1
@@ -184,93 +143,6 @@
}); });
} }
function preparePlayerCountData() {
// Group by date and get average player count per day
const dailyData = stateHistory.reduce(
(acc, item) => {
const date = formatDate(item.dateCreated, 'MMM dd kk:mm');
if (!acc[date]) {
acc[date] = { total: 0, count: 0 };
}
acc[date].total = acc[date].total > item.playerCount ? acc[date].total : item.playerCount;
return acc;
},
{} as Record<string, { total: number; count: number }>
);
return Object.entries(dailyData)
.map(([date, data]) => ({
x: date,
y: data.total
}))
.sort((a, b) => compareAsc(a.x, b.x));
}
function prepareSessionTypeData() {
const sessionCounts = stateHistory.reduce(
(acc, session) => {
acc[session.session] = (acc[session.session] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return {
labels: Object.keys(sessionCounts),
data: Object.values(sessionCounts)
};
}
function prepareDailyActivityData() {
const dailyActivity = stateHistory.reduce(
(acc, session, index) => {
const date = formatDate(session.dateCreated, 'yyyy-MM-dd');
// Initialize counter for this date if not exists
if (!acc[date]) {
acc[date] = {
count: 0,
sessions: []
};
}
// Check if this session is part of a sequence
if (index > 0) {
const prevSession = stateHistory[index - 1];
if (
isSameDay(session.dateCreated, prevSession.dateCreated) &&
prevSession.session === session.session
) {
return acc;
}
}
// Increment counter for non-sequential sessions
acc[date].count++;
acc[date].sessions.push(session);
return acc;
},
{} as Record<
string,
{
count: number;
sessions: StateHistory[];
}
>
);
const sortedEntries = Object.entries(dailyActivity).sort(
([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
);
console.log(sortedEntries);
return {
labels: sortedEntries.map(([date]) => formatDate(date, 'MMM dd')),
data: sortedEntries.map(([, count]) => count)
};
}
function formatDate(dateString: string, formatString: string) { function formatDate(dateString: string, formatString: string) {
return formatInTimeZone(dateString, localTimeZone, formatString, { return formatInTimeZone(dateString, localTimeZone, formatString, {
timeZone: 'utc' timeZone: 'utc'
@@ -306,7 +178,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-400">Average Players</p> <p class="text-sm font-medium text-gray-400">Average Players</p>
<p class="text-2xl font-bold text-white">{averagePlayerCount}</p> <p class="text-2xl font-bold text-white">{Math.round(statistics.averagePlayers)}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -331,7 +203,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-400">Peak Players</p> <p class="text-sm font-medium text-gray-400">Peak Players</p>
<p class="text-2xl font-bold text-white">{peakPlayerCount}</p> <p class="text-2xl font-bold text-white">{statistics.peakPlayers}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -356,7 +228,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-400">Total Sessions</p> <p class="text-sm font-medium text-gray-400">Total Sessions</p>
<p class="text-2xl font-bold text-white">{totalSessions}</p> <p class="text-2xl font-bold text-white">{statistics.totalSessions}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -381,7 +253,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-400">Total Playtime</p> <p class="text-sm font-medium text-gray-400">Total Playtime</p>
<p class="text-2xl font-bold text-white">{formatDuration(totalPlaytime)}</p> <p class="text-2xl font-bold text-white">{formatDuration(statistics.totalPlaytime)}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -439,32 +311,32 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-700 bg-gray-800"> <tbody class="divide-y divide-gray-700 bg-gray-800">
{#each flatMap(dailyActivityData?.data, ({ sessions }) => sessions) as session} {#each statistics.recentSessions as session}
<tr> <tr>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300"> <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{formatDate(session.dateCreated, 'MMM dd kk:mm')} {formatDate(session.date, 'MMM dd kk:mm')}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span <span
class={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${ class={`inline-flex rounded-full px-2 py-1 text-xs font-semibold ${
session.session === 'Race' session.type === 'Race'
? 'bg-red-100 text-red-800' ? 'bg-red-100 text-red-800'
: session.session === 'Qualifying' : session.type === 'Qualifying'
? 'bg-yellow-100 text-yellow-800' ? 'bg-yellow-100 text-yellow-800'
: 'bg-green-100 text-green-800' : 'bg-green-100 text-green-800'
}`} }`}
> >
{session.session} {session.type}
</span> </span>
</td> </td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300"> <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{session.track} {session.track}
</td> </td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300"> <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{formatDuration(session.sessionDurationMinutes)} {formatDuration(session.duration)}
</td> </td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300"> <td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{session.playerCount} {session.players}
</td> </td>
</tr> </tr>
{/each} {/each}

View File

@@ -15,6 +15,7 @@ export enum configFile {
} }
export enum serverTab { export enum serverTab {
statistics = 'statistics', statistics = 'statistics',
statistics2 = 'statistics2',
configuration = 'configuration', configuration = 'configuration',
assistRules = 'assistRules', assistRules = 'assistRules',
event = 'event', event = 'event',
@@ -31,6 +32,41 @@ export interface StateHistory {
session: string; session: string;
} }
interface SessionCount {
name: string;
count: number;
}
interface DailyActivity {
date: string; // ISO 8601 date string
sessionsCount: number;
}
interface PlayerCountPoint {
timestamp: string; // ISO 8601 datetime string
count: number;
}
interface RecentSession {
id: number;
date: string;
type: string;
track: string;
duration: number;
players: number;
}
export interface StateHistoryStats {
averagePlayers: number;
peakPlayers: number;
totalSessions: number;
totalPlaytime: number; // in minutes
playerCountOverTime: PlayerCountPoint[];
sessionTypes: SessionCount[];
dailyActivity: DailyActivity[];
recentSessions: RecentSession[];
}
export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings; export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings;
export type ConfigFile = export type ConfigFile =
| configFile.configuration | configFile.configuration

View File

@@ -1,4 +1,10 @@
import { updateConfig, getConfigFiles, getServerById, getStateHistory } from '$api/serverService'; import {
updateConfig,
getConfigFiles,
getServerById,
getStateHistory,
getStateHistoryStats
} from '$api/serverService';
import type { Actions } from './$types'; import type { Actions } from './$types';
import { checkAuth } from '$api/authService'; import { checkAuth } from '$api/authService';
import { getTracks } from '$api/lookupService'; import { getTracks } from '$api/lookupService';
@@ -17,18 +23,18 @@ export const load = async (event: RequestEvent) => {
const endDate = formatISO(today); const endDate = formatISO(today);
const startDate = formatISO(subDays(today, 30)); const startDate = formatISO(subDays(today, 30));
const [server, configs, tracks, stateHistory] = await Promise.all([ const [server, configs, tracks, statistics] = await Promise.all([
getServerById(event, event.params.id), getServerById(event, event.params.id),
getConfigFiles(event, event.params.id), getConfigFiles(event, event.params.id),
getTracks(event), getTracks(event),
getStateHistory(event, event.params.id, startDate, endDate) getStateHistoryStats(event, event.params.id, startDate, endDate)
]); ]);
return { return {
id: event.params.id, id: event.params.id,
configs, configs,
tracks, tracks,
server, server,
stateHistory statistics
}; };
}; };

View File

@@ -13,7 +13,7 @@
const tracks = data.tracks; const tracks = data.tracks;
const id = data.id; const id = data.id;
const server = data.server; const server = data.server;
const stateHistory = data.stateHistory; const statistics = data.statistics;
let tab = $state(serverTab.statistics); let tab = $state(serverTab.statistics);
</script> </script>
@@ -175,7 +175,7 @@
</header> </header>
<div class="p-6"> <div class="p-6">
{#if tab === serverTab.statistics} {#if tab === serverTab.statistics}
<Statistics {stateHistory} /> <Statistics {statistics} />
{:else if tab === serverTab.event} {:else if tab === serverTab.event}
<EditorEvent config={configs.event} {tracks} {id} /> <EditorEvent config={configs.event} {tracks} {id} />
{:else if tab === serverTab.configuration} {:else if tab === serverTab.configuration}