fetch statistics from the server
This commit is contained in:
@@ -9,7 +9,8 @@ import {
|
||||
type EventConfig,
|
||||
type EventRules,
|
||||
type ServerSettings,
|
||||
type StateHistory
|
||||
type StateHistory,
|
||||
type StateHistoryStats
|
||||
} from '$models/config';
|
||||
import type { Server } from '$models/server';
|
||||
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> => {
|
||||
return fetchAPIEvent(event, `/server/${serverId}/config/${configFile.event}`);
|
||||
};
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
import { compareAsc, isSameDay } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import type { StateHistory } from '$models/config';
|
||||
import type { StateHistoryStats } from '$models/config';
|
||||
import { flatMap } from 'lodash-es';
|
||||
|
||||
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
interface Props {
|
||||
// Props
|
||||
stateHistory?: Array<StateHistory>;
|
||||
statistics: StateHistoryStats;
|
||||
}
|
||||
|
||||
let { stateHistory = [] }: Props = $props();
|
||||
let { statistics }: Props = $props();
|
||||
|
||||
// Chart instances
|
||||
let playerCountChart: Chart | null = null;
|
||||
@@ -25,21 +25,8 @@
|
||||
let sessionTypeCanvas: 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)
|
||||
onMount(() => {
|
||||
processData();
|
||||
createCharts();
|
||||
});
|
||||
|
||||
@@ -50,43 +37,19 @@
|
||||
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() {
|
||||
if (!playerCountCanvas || !sessionTypeCanvas || !dailyActivityCanvas) return;
|
||||
if (!statistics || !playerCountCanvas || !sessionTypeCanvas || !dailyActivityCanvas) return;
|
||||
|
||||
// Player Count Over Time Chart
|
||||
const playerCountData = preparePlayerCountData();
|
||||
playerCountChart = new Chart(playerCountCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: playerCountData.map(({ x }) => x),
|
||||
labels: statistics.playerCountOverTime.map(({ timestamp }) =>
|
||||
formatDate(timestamp, 'MMM dd kk:mm')
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Player Count',
|
||||
data: playerCountData.map(({ y }) => y),
|
||||
data: statistics.playerCountOverTime.map(({ count }) => count),
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
fill: true,
|
||||
@@ -107,15 +70,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Session Types Pie Chart
|
||||
const sessionTypeData = prepareSessionTypeData();
|
||||
sessionTypeChart = new Chart(sessionTypeCanvas, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: sessionTypeData.labels,
|
||||
labels: statistics.sessionTypes.map(({ name }) => name),
|
||||
datasets: [
|
||||
{
|
||||
data: sessionTypeData.data,
|
||||
data: statistics.sessionTypes.map(({ count }) => count),
|
||||
backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
|
||||
}
|
||||
]
|
||||
@@ -135,16 +96,14 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Daily Activity Bar Chart
|
||||
dailyActivityData = prepareDailyActivityData();
|
||||
dailyActivityChart = new Chart(dailyActivityCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: dailyActivityData.labels,
|
||||
labels: statistics.dailyActivity.map(({ date }) => date),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Sessions',
|
||||
data: dailyActivityData.data.map(({ count }) => count),
|
||||
data: statistics.dailyActivity.map(({ sessionsCount }) => sessionsCount),
|
||||
backgroundColor: '#10b981',
|
||||
borderColor: '#059669',
|
||||
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) {
|
||||
return formatInTimeZone(dateString, localTimeZone, formatString, {
|
||||
timeZone: 'utc'
|
||||
@@ -306,7 +178,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<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>
|
||||
@@ -331,7 +203,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<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>
|
||||
@@ -356,7 +228,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<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>
|
||||
@@ -381,7 +253,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<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>
|
||||
@@ -439,32 +311,32 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700 bg-gray-800">
|
||||
{#each flatMap(dailyActivityData?.data, ({ sessions }) => sessions) as session}
|
||||
{#each statistics.recentSessions as session}
|
||||
<tr>
|
||||
<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 class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
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'
|
||||
: session.session === 'Qualifying'
|
||||
: session.type === 'Qualifying'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}
|
||||
>
|
||||
{session.session}
|
||||
{session.type}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
|
||||
{session.track}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
|
||||
{formatDuration(session.sessionDurationMinutes)}
|
||||
{formatDuration(session.duration)}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
|
||||
{session.playerCount}
|
||||
{session.players}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -15,6 +15,7 @@ export enum configFile {
|
||||
}
|
||||
export enum serverTab {
|
||||
statistics = 'statistics',
|
||||
statistics2 = 'statistics2',
|
||||
configuration = 'configuration',
|
||||
assistRules = 'assistRules',
|
||||
event = 'event',
|
||||
@@ -31,6 +32,41 @@ export interface StateHistory {
|
||||
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 ConfigFile =
|
||||
| configFile.configuration
|
||||
|
||||
@@ -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 { checkAuth } from '$api/authService';
|
||||
import { getTracks } from '$api/lookupService';
|
||||
@@ -17,18 +23,18 @@ export const load = async (event: RequestEvent) => {
|
||||
const endDate = formatISO(today);
|
||||
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),
|
||||
getConfigFiles(event, event.params.id),
|
||||
getTracks(event),
|
||||
getStateHistory(event, event.params.id, startDate, endDate)
|
||||
getStateHistoryStats(event, event.params.id, startDate, endDate)
|
||||
]);
|
||||
return {
|
||||
id: event.params.id,
|
||||
configs,
|
||||
tracks,
|
||||
server,
|
||||
stateHistory
|
||||
statistics
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
const tracks = data.tracks;
|
||||
const id = data.id;
|
||||
const server = data.server;
|
||||
const stateHistory = data.stateHistory;
|
||||
const statistics = data.statistics;
|
||||
let tab = $state(serverTab.statistics);
|
||||
</script>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
</header>
|
||||
<div class="p-6">
|
||||
{#if tab === serverTab.statistics}
|
||||
<Statistics {stateHistory} />
|
||||
<Statistics {statistics} />
|
||||
{:else if tab === serverTab.event}
|
||||
<EditorEvent config={configs.event} {tracks} {id} />
|
||||
{:else if tab === serverTab.configuration}
|
||||
|
||||
Reference in New Issue
Block a user