diff --git a/src/api/serverService.ts b/src/api/serverService.ts index 3de6b8f..42db858 100644 --- a/src/api/serverService.ts +++ b/src/api/serverService.ts @@ -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 => { + return fetchAPIEvent( + event, + `/server/${serverId}/state-history/statistics?start_date=${startDate}&end_date=${endDate}` + ); +}; + export const getEventFile = async (event: RequestEvent, serverId: string): Promise => { return fetchAPIEvent(event, `/server/${serverId}/config/${configFile.event}`); }; diff --git a/src/components/Statistics.svelte b/src/components/Statistics.svelte index 96bd215..eae7793 100644 --- a/src/components/Statistics.svelte +++ b/src/components/Statistics.svelte @@ -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; + 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 - ); - - 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 - ); - - 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 @@

Average Players

-

{averagePlayerCount}

+

{Math.round(statistics.averagePlayers)}

@@ -331,7 +203,7 @@

Peak Players

-

{peakPlayerCount}

+

{statistics.peakPlayers}

@@ -356,7 +228,7 @@

Total Sessions

-

{totalSessions}

+

{statistics.totalSessions}

@@ -381,7 +253,7 @@

Total Playtime

-

{formatDuration(totalPlaytime)}

+

{formatDuration(statistics.totalPlaytime)}

@@ -439,32 +311,32 @@ - {#each flatMap(dailyActivityData?.data, ({ sessions }) => sessions) as session} + {#each statistics.recentSessions as session} - {formatDate(session.dateCreated, 'MMM dd kk:mm')} + {formatDate(session.date, 'MMM dd kk:mm')} - {session.session} + {session.type} {session.track} - {formatDuration(session.sessionDurationMinutes)} + {formatDuration(session.duration)} - {session.playerCount} + {session.players} {/each} diff --git a/src/models/config.ts b/src/models/config.ts index 196a216..eca13ef 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -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 diff --git a/src/routes/dashboard/server/[id]/+page.server.ts b/src/routes/dashboard/server/[id]/+page.server.ts index f465d31..7669d93 100644 --- a/src/routes/dashboard/server/[id]/+page.server.ts +++ b/src/routes/dashboard/server/[id]/+page.server.ts @@ -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 }; }; diff --git a/src/routes/dashboard/server/[id]/+page.svelte b/src/routes/dashboard/server/[id]/+page.svelte index 14e249f..e143e19 100644 --- a/src/routes/dashboard/server/[id]/+page.svelte +++ b/src/routes/dashboard/server/[id]/+page.svelte @@ -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); @@ -175,7 +175,7 @@
{#if tab === serverTab.statistics} - + {:else if tab === serverTab.event} {:else if tab === serverTab.configuration}