add statistics

This commit is contained in:
Fran Jurmanović
2025-05-29 00:21:58 +02:00
parent 4f0e93e60d
commit 7b50ac4b32
7 changed files with 681 additions and 35 deletions

105
package-lock.json generated
View File

@@ -7,6 +7,13 @@
"": {
"name": "acc-server-manager-web",
"version": "0.0.1",
"dependencies": {
"@date-fns/tz": "^1.2.0",
"chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
@@ -18,6 +25,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.4",
"@tailwindcss/vite": "^4.0.0",
"@types/d3-scale": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
@@ -64,6 +72,12 @@
"node": ">=6.0.0"
}
},
"node_modules/@date-fns/tz": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
@@ -798,6 +812,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1662,6 +1682,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1693,6 +1730,18 @@
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "22.15.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz",
"integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2057,6 +2106,28 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chart.js": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs-adapter-date-fns": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=2.8.0",
"date-fns": ">=2.0.0"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -2165,6 +2236,25 @@
"node": ">=4"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -2602,9 +2692,9 @@
}
},
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
"integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -4326,6 +4416,15 @@
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@@ -24,6 +24,7 @@
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.4",
"@tailwindcss/vite": "^4.0.0",
"@types/d3-scale": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
@@ -41,5 +42,12 @@
"typescript-eslint": "^8.20.0",
"uuid": "^11.0.5",
"vite": "^6.0.0"
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
"chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0"
}
}

View File

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

View File

@@ -0,0 +1,471 @@
<script lang="ts">
import Chart from 'chart.js/auto';
import { onMount, onDestroy } from 'svelte';
import { compareAsc } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import 'chartjs-adapter-date-fns';
import type { StateHistory } from '$models/config';
import { flatMap } from 'lodash-es';
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
interface Props {
// Props
stateHistory?: Array<StateHistory>;
}
let { stateHistory = [] }: Props = $props();
// Chart instances
let playerCountChart: Chart | null = null;
let sessionTypeChart: Chart | null = null;
let dailyActivityChart: Chart | null = null;
// Chart canvas elements
let playerCountCanvas: HTMLCanvasElement | undefined = $state();
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();
});
onDestroy(() => {
// Cleanup charts
if (playerCountChart) playerCountChart.destroy();
if (sessionTypeChart) sessionTypeChart.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() {
if (!playerCountCanvas || !sessionTypeCanvas || !dailyActivityCanvas) return;
// Player Count Over Time Chart
const playerCountData = preparePlayerCountData();
playerCountChart = new Chart(playerCountCanvas, {
type: 'line',
data: {
labels: playerCountData.map(({ x }) => x),
datasets: [
{
label: 'Player Count',
data: playerCountData.map(({ y }) => y),
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#ffffff'
}
}
}
}
});
// Session Types Pie Chart
const sessionTypeData = prepareSessionTypeData();
sessionTypeChart = new Chart(sessionTypeCanvas, {
type: 'doughnut',
data: {
labels: sessionTypeData.labels,
datasets: [
{
data: sessionTypeData.data,
backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#ffffff',
padding: 20
}
}
}
}
});
// Daily Activity Bar Chart
dailyActivityData = prepareDailyActivityData();
dailyActivityChart = new Chart(dailyActivityCanvas, {
type: 'bar',
data: {
labels: dailyActivityData.labels,
datasets: [
{
label: 'Sessions',
data: dailyActivityData.data.map(({ count }) => count),
backgroundColor: '#10b981',
borderColor: '#059669',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#ffffff'
}
}
},
scales: {
x: {
ticks: {
color: '#9ca3af'
},
grid: {
color: '#374151'
}
},
y: {
beginAtZero: true,
ticks: {
color: '#9ca3af'
},
grid: {
color: '#374151'
}
}
}
}
});
}
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: [session]
};
}
// Check if this session is part of a sequence
if (index > 0) {
const prevSession = stateHistory[index - 1];
const prevDate = formatDate(prevSession.dateCreated, 'yyyy-MM-dd');
if (date === prevDate && 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()
);
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'
});
}
function formatDuration(minutes: number) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
}
</script>
<!-- Summary Cards -->
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-4">
<div class="rounded-lg bg-gray-800 p-6">
<div class="flex items-center">
<div class="rounded-lg bg-green-600 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</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>
</div>
</div>
</div>
<div class="rounded-lg bg-gray-800 p-6">
<div class="flex items-center">
<div class="rounded-lg bg-yellow-600 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
</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>
</div>
</div>
</div>
<div class="rounded-lg bg-gray-800 p-6">
<div class="flex items-center">
<div class="rounded-lg bg-blue-600 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</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>
</div>
</div>
</div>
<div class="rounded-lg bg-gray-800 p-6">
<div class="flex items-center">
<div class="rounded-lg bg-purple-600 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</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>
</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Player Count Over Time -->
<div class="rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold">Player Count Over Time</h3>
<div class="h-64">
<canvas bind:this={playerCountCanvas}></canvas>
</div>
</div>
<!-- Session Types Pie Chart -->
<div class="rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold">Session Types</h3>
<div class="h-64">
<canvas bind:this={sessionTypeCanvas}></canvas>
</div>
</div>
</div>
<!-- Daily Activity Bar Chart -->
<div class="mb-8 rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold">Daily Activity</h3>
<div class="h-64">
<canvas bind:this={dailyActivityCanvas}></canvas>
</div>
</div>
<!-- Session History Table -->
<div class="rounded-lg bg-gray-800 p-6">
<h3 class="mb-4 text-lg font-semibold">Recent Sessions</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-300 uppercase"
>Date</th
>
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-300 uppercase"
>Type</th
>
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-300 uppercase"
>Track</th
>
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-300 uppercase"
>Duration</th
>
<th class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-300 uppercase"
>Players</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-700 bg-gray-800">
{#each flatMap(dailyActivityData?.data, ({ sessions }) => sessions) as session}
<tr>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{formatDate(session.dateCreated, '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'
? 'bg-red-100 text-red-800'
: session.session === 'Qualifying'
? 'bg-yellow-100 text-yellow-800'
: 'bg-green-100 text-green-800'
}`}
>
{session.session}
</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)}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-300">
{session.playerCount}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>

View File

@@ -13,6 +13,23 @@ export enum configFile {
eventRules = 'eventRules.json',
settings = 'settings.json'
}
export enum serverTab {
statistics = 'statistics',
configuration = 'configuration',
assistRules = 'assistRules',
event = 'event',
eventRules = 'eventRules',
settings = 'settings'
}
export interface StateHistory {
dateCreated: string;
sessionStart: string;
playerCount: number;
track: string;
sessionDurationMinutes: number;
session: string;
}
export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings;
export type ConfigFile =

View File

@@ -1,4 +1,4 @@
import { updateConfig, getConfigFiles, getServerById } from '$api/serverService';
import { updateConfig, getConfigFiles, getServerById, getStateHistory } from '$api/serverService';
import type { Actions } from './$types';
import { checkAuth } from '$api/authService';
import { getTracks } from '$api/lookupService';
@@ -6,21 +6,30 @@ import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { configFile, type Config, type Session } from '$models/config';
import { set } from 'lodash-es';
import { format } from 'date-fns-tz';
import { subDays } from 'date-fns';
export const load = async (event: RequestEvent) => {
const isAuth = await checkAuth(event);
if (!isAuth) return redirect(308, '/login');
if (!event.params.id) return redirect(308, '/dashboard');
const [server, configs, tracks] = await Promise.all([
const today = format(new Date(), 'yyyy-MM-ddTHH:mm:ssZ');
const thirtyDaysAgo = format(subDays(new Date(), 30), 'yyyy-MM-ddTHH:mm:ssZ');
const endDate = today;
const startDate = thirtyDaysAgo;
const [server, configs, tracks, stateHistory] = await Promise.all([
getServerById(event, event.params.id),
getConfigFiles(event, event.params.id),
getTracks(event)
getTracks(event),
getStateHistory(event, event.params.id, startDate, endDate)
]);
return {
id: event.params.id,
configs,
tracks,
server
server,
stateHistory
};
};

View File

@@ -4,15 +4,17 @@
import EditorEvent from '$components/EditorEvent.svelte';
import EditorEventRules from '$components/EditorEventRules.svelte';
import EditorSettings from '$components/EditorSettings.svelte';
import Statistics from '$components/Statistics.svelte';
import { getStatusColor, serviceStatusToString } from '$lib/types/serviceStatus.js';
import { configFile } from '$models/config.js';
import { serverTab } from '$models/config.js';
let { data } = $props();
const configs = data.configs;
const tracks = data.tracks;
const id = data.id;
const server = data.server;
let tab = $state(configFile.event);
const stateHistory = data.stateHistory;
let tab = $state(serverTab.statistics);
</script>
<svelte:head>
@@ -37,27 +39,50 @@
<ul class="space-y-1">
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === configFile.event ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === configFile.event || !configs.event}
onclick={() => (tab = configFile.event)}
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.event ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.event || !configs.event}
onclick={() => (tab = serverTab.event)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 inline h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
Statistics
</button>
</li>
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.event ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.event || !configs.event}
onclick={() => (tab = serverTab.event)}
>
Event
</button>
</li>
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === configFile.configuration ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === configFile.configuration || !configs.configuration}
onclick={() => (tab = configFile.configuration)}
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.configuration ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.configuration || !configs.configuration}
onclick={() => (tab = serverTab.configuration)}
>
Configuration
</button>
</li>
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === configFile.settings ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === configFile.settings || !configs.settings}
onclick={() => (tab = configFile.settings)}
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.settings ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.settings || !configs.settings}
onclick={() => (tab = serverTab.settings)}
>
Settings
</button>
@@ -65,9 +90,9 @@
{#if configs.assistRules}
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === configFile.assistRules ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === configFile.assistRules || !configs.assistRules}
onclick={() => (tab = configFile.assistRules)}
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.assistRules ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.assistRules || !configs.assistRules}
onclick={() => (tab = serverTab.assistRules)}
>
Assist Rules
</button>
@@ -76,9 +101,9 @@
{#if configs.eventRules}
<li>
<button
class={`w-full rounded-md px-3 py-2 text-left ${tab === configFile.eventRules ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === configFile.eventRules || !configs.eventRules}
onclick={() => (tab = configFile.eventRules)}
class={`w-full rounded-md px-3 py-2 text-left ${tab === serverTab.eventRules ? 'bg-green-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
disabled={tab === serverTab.eventRules || !configs.eventRules}
onclick={() => (tab = serverTab.eventRules)}
>
Event Rules
</button>
@@ -91,15 +116,17 @@
<main class="flex-1 overflow-auto">
<header class="flex items-center justify-between bg-gray-800 p-4 shadow-md">
<h1 class="text-xl font-semibold">
{#if tab === configFile.event}
{#if tab === serverTab.statistics}
Statistics
{:else if tab === serverTab.event}
Event
{:else if tab === configFile.configuration}
{:else if tab === serverTab.configuration}
Configuration
{:else if tab === configFile.settings}
{:else if tab === serverTab.settings}
Settings
{:else if tab === configFile.assistRules}
{:else if tab === serverTab.assistRules}
Assist Rules
{:else if tab === configFile.eventRules}
{:else if tab === serverTab.eventRules}
Event Rules
{/if}
</h1>
@@ -147,15 +174,17 @@
</div>
</header>
<div class="p-6">
{#if tab === configFile.event}
{#if tab === serverTab.statistics}
<Statistics {stateHistory} />
{:else if tab === serverTab.event}
<EditorEvent config={configs.event} {tracks} {id} />
{:else if tab === configFile.configuration}
{:else if tab === serverTab.configuration}
<EditorConfiguration config={configs.configuration} {id} />
{:else if tab === configFile.settings}
{:else if tab === serverTab.settings}
<EditorSettings config={configs.settings} {id} />
{:else if tab === configFile.assistRules}
{:else if tab === serverTab.assistRules}
<EditorAssistRules config={configs.assistRules} {id} />
{:else if tab === configFile.eventRules}
{:else if tab === serverTab.eventRules}
<EditorEventRules config={configs.eventRules} {id} />
{:else}
<div class="rounded-lg bg-gray-800 p-6 text-center">